tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.moreDebug = False 300 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 301 302 self.historyFile = None 303 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 304 305 See also: `History()`. 306 """ 307 308 self.htmlHistoryFile = "index.html" 309 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 310 311 See also: `ShowHistoryChart()`. 312 """ 313 314 self.instrumentsFile = "instruments.md" 315 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 316 317 See also: `ShowInstrumentsInfo()`. 318 """ 319 320 self.searchResultsFile = "search-results.md" 321 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 322 323 See also: `SearchInstruments()`. 324 """ 325 326 self.pricesFile = "prices.md" 327 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 328 329 See also: `GetListOfPrices()`. 330 """ 331 332 self.infoFile = "info.md" 333 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 334 335 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 336 """ 337 338 self.bondsXLSXFile = "ext-bonds.xlsx" 339 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 340 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 341 342 See also: `ExtendBondsData()`. 343 """ 344 345 self.calendarFile = "calendar.md" 346 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 347 348 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 349 350 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 351 """ 352 353 self.overviewFile = "overview.md" 354 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 355 356 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 357 """ 358 359 self.overviewDigestFile = "overview-digest.md" 360 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 361 362 See also: `Overview()` with parameter `details="digest"`. 363 """ 364 365 self.overviewPositionsFile = "overview-positions.md" 366 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 367 368 See also: `Overview()` with parameter `details="positions"`. 369 """ 370 371 self.overviewOrdersFile = "overview-orders.md" 372 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 373 374 See also: `Overview()` with parameter `details="orders"`. 375 """ 376 377 self.overviewAnalyticsFile = "overview-analytics.md" 378 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 379 380 See also: `Overview()` with parameter `details="analytics"`. 381 """ 382 383 self.reportFile = "deals.md" 384 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 385 386 See also: `Deals()`. 387 """ 388 389 self.withdrawalLimitsFile = "limits.md" 390 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 391 392 See also: `OverviewLimits()` and `RequestLimits()`. 393 """ 394 395 self.userInfoFile = "user-info.md" 396 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 397 398 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 399 """ 400 401 self.userAccountsFile = "accounts.md" 402 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 403 404 See also: `OverviewAccounts()`, `RequestAccounts()`. 405 """ 406 407 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 408 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 409 410 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 411 412 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 413 """ 414 415 self.iList = None # init iList for raw instruments data 416 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 417 418 See also: `Listing()`, `DumpInstruments()`. 419 """ 420 421 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 422 if useCache: 423 if os.path.exists(self.iListDumpFile): 424 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 425 curTime = datetime.now(tzutc()) 426 427 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 428 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 429 430 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 431 432 else: 433 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 434 435 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 436 os.path.abspath(self.iListDumpFile), 437 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 438 )) 439 440 else: 441 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 442 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 443 444 else: 445 self.iList = self.Listing() # request new raw instruments data from broker server 446 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 447 448 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 449 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 450 451 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 452 """ 453 454 def _ParseJSON(self, rawData="{}") -> dict: 455 """ 456 Parse JSON from response string. 457 458 :param rawData: this is a string with JSON-formatted text. 459 :return: JSON (dictionary), parsed from server response string. 460 """ 461 responseJSON = json.loads(rawData) if rawData else {} 462 463 if self.moreDebug: 464 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 465 466 return responseJSON 467 468 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 469 """ 470 Send GET or POST request to broker server and receive JSON object. 471 472 self.header: must be defining with dictionary of headers. 473 self.body: if define then used as request body. None by default. 474 self.timeout: global request timeout, 15 seconds by default. 475 :param url: url with REST request. 476 :param reqType: send "GET" or "POST" request. "GET" by default. 477 :param retry: how many times retry after first request if an 5xx server errors occurred. 478 :param pause: sleep time in seconds between retries. 479 :return: response JSON (dictionary) from broker. 480 """ 481 if reqType not in ("GET", "POST"): 482 uLogger.error("You can define request type: 'GET' or 'POST'!") 483 raise Exception("Incorrect value") 484 485 if self.moreDebug: 486 uLogger.debug("Request parameters:") 487 uLogger.debug(" - REST API URL: {}".format(url)) 488 uLogger.debug(" - request type: {}".format(reqType)) 489 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 490 uLogger.debug(" - body:\n{}".format(self.body)) 491 492 # fast hack to avoid all operations with some tickers/FIGI 493 responseJSON = {} 494 oK = True 495 for item in self.exclude: 496 if item in url: 497 if self.moreDebug: 498 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 499 500 oK = False 501 break 502 503 if oK: 504 counter = 0 505 response = None 506 errMsg = "" 507 508 while not response and counter <= retry: 509 if reqType == "GET": 510 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 511 512 if reqType == "POST": 513 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 514 515 if self.moreDebug: 516 uLogger.debug("Response:") 517 uLogger.debug(" - status code: {}".format(response.status_code)) 518 uLogger.debug(" - reason: {}".format(response.reason)) 519 uLogger.debug(" - body length: {}".format(len(response.text))) 520 uLogger.debug(" - headers:\n{}".format(response.headers)) 521 522 # Server returns some headers: 523 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 524 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 525 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 526 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 527 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 528 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 529 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 530 sleep(rateLimitWait) 531 532 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 533 if 400 <= response.status_code < 500: 534 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 535 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 536 counter = retry + 1 537 538 if 500 <= response.status_code < 600: 539 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, {}".format(errMsg)) 541 counter += 1 542 543 if counter <= retry: 544 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 545 sleep(pause) 546 547 responseJSON = self._ParseJSON(rawData=response.text) 548 549 if errMsg: 550 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 551 uLogger.error(" - not oK, {}".format(errMsg)) 552 553 return responseJSON 554 555 def _IUpdater(self, iType: str) -> tuple: 556 """ 557 Request instrument by type from server. See available API methods for instruments: 558 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 559 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 560 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 561 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 562 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 563 564 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 565 :return: tuple with iType name and list of available instruments of current type for defined user token. 566 """ 567 result = [] 568 569 if iType in TKS_INSTRUMENTS: 570 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 571 572 # all instruments have the same body in API v2 requests: 573 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 574 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 575 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 576 577 return iType, result 578 579 def _IWrapper(self, kwargs): 580 """ 581 Wrapper runs instrument's update method `_IUpdater()`. 582 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 583 """ 584 return self._IUpdater(**kwargs) 585 586 def Listing(self) -> dict: 587 """ 588 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 589 590 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 591 """ 592 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 593 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 594 595 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 596 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 597 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 598 599 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 600 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 601 poolUpdater.close() 602 603 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 604 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 605 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 606 607 # calculate minimum price increment (step) for all instruments and set up instrument's type: 608 for iType in iList.keys(): 609 for ticker in iList[iType]: 610 iList[iType][ticker]["type"] = iType 611 612 if "minPriceIncrement" in iList[iType][ticker].keys(): 613 iList[iType][ticker]["step"] = NanoToFloat( 614 iList[iType][ticker]["minPriceIncrement"]["units"], 615 iList[iType][ticker]["minPriceIncrement"]["nano"], 616 ) 617 618 else: 619 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 620 621 return iList 622 623 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 624 """ 625 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 626 627 See also: `DumpInstruments()`, `Listing()`. 628 629 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 630 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 631 """ 632 if self.iListDumpFile is None or not self.iListDumpFile: 633 uLogger.error("Output name of dump file must be defined!") 634 raise Exception("Filename required") 635 636 if not self.iList or forceUpdate: 637 self.iList = self.Listing() 638 639 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 640 641 # Save as XLSX with separated sheets for every type of instruments: 642 with pd.ExcelWriter( 643 path=xlsxDumpFile, 644 date_format=TKS_DATE_FORMAT, 645 datetime_format=TKS_DATE_TIME_FORMAT, 646 mode="w", 647 ) as writer: 648 for iType in TKS_INSTRUMENTS: 649 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 650 df = df[sorted(df)] # sorted by column names 651 df = df.applymap( 652 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 653 na_action="ignore", 654 ) # converting numbers from nano-type to float in every cell 655 df.to_excel( 656 writer, 657 sheet_name=iType, 658 encoding="UTF-8", 659 freeze_panes=(1, 1), 660 ) # saving as XLSX-file with freeze first row and column as headers 661 662 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 663 664 def DumpInstruments(self, forceUpdate: bool = True) -> str: 665 """ 666 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 667 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 668 669 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 670 671 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 672 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 673 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 674 """ 675 if self.iListDumpFile is None or not self.iListDumpFile: 676 uLogger.error("Output name of dump file must be defined!") 677 raise Exception("Filename required") 678 679 if not self.iList or forceUpdate: 680 self.iList = self.Listing() 681 682 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 683 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 684 fH.write(jsonDump) 685 686 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 687 688 return jsonDump 689 690 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 691 """ 692 Show information about one instrument defined by json data and prints it in Markdown format. 693 694 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 695 696 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 697 :param show: if `True` then also printing information about instrument and its current price. 698 :return: multilines text in Markdown format with information about one instrument. 699 """ 700 splitLine = "| | |\n" 701 infoText = "" 702 703 if iJSON is not None and iJSON and isinstance(iJSON, dict): 704 info = [ 705 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 706 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 707 "| Parameters | Values |\n", 708 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 709 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 710 "| Full name: | {:<54} |\n".format(iJSON["name"]), 711 ] 712 713 if "sector" in iJSON.keys() and iJSON["sector"]: 714 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 715 716 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 717 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 718 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 719 ))) 720 721 info.extend([ 722 splitLine, 723 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 724 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 725 ]) 726 727 if "isin" in iJSON.keys() and iJSON["isin"]: 728 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 729 730 if "classCode" in iJSON.keys(): 731 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 732 733 info.extend([ 734 splitLine, 735 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 736 splitLine, 737 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 738 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 739 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 740 ]) 741 742 if iJSON["figi"]: 743 self.figi = iJSON["figi"] 744 iJSON = iJSON | self.RequestTradingStatus() 745 746 info.extend([ 747 splitLine, 748 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 749 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 750 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 751 ]) 752 753 info.append(splitLine) 754 755 if "type" in iJSON.keys() and iJSON["type"]: 756 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 757 758 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 759 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 760 761 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 762 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 763 764 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 765 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 766 767 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 768 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 769 770 if "focusType" in iJSON.keys() and iJSON["focusType"]: 771 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 772 773 if "assetType" in iJSON.keys() and iJSON["assetType"]: 774 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 775 776 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 777 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 778 779 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 780 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 781 782 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 783 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 784 785 if "currency" in iJSON.keys(): 786 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 787 788 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 789 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 790 791 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 792 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 793 794 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 795 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 796 797 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 798 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 799 800 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 801 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 802 803 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 804 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 805 806 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 807 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 808 809 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 810 info.append("| Perpetual bond: | Yes |\n") 811 812 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 813 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 814 815 iExt = None 816 if iJSON["type"] == "Bonds": 817 info.extend([ 818 splitLine, 819 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 820 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 821 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 822 iJSON["nominal"]["currency"], 823 )), 824 ]) 825 826 if "floatingCouponFlag" in iJSON.keys(): 827 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 828 829 if "amortizationFlag" in iJSON.keys(): 830 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 831 832 info.append(splitLine) 833 834 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 835 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 836 837 if iJSON["figi"]: 838 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 839 840 info.extend([ 841 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 842 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 843 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 844 ]) 845 846 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 847 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 848 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 849 iJSON["aciValue"]["currency"] 850 ))) 851 852 if "currentPrice" in iJSON.keys(): 853 info.append(splitLine) 854 855 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 856 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 857 858 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 859 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 860 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 861 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 862 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 863 864 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 865 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 866 867 info.extend([ 868 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 869 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 870 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 871 )), 872 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Changes between last deal price and last close | {:<54} |\n".format( 877 "{:.2f}%{}".format( 878 iJSON["currentPrice"]["changes"], 879 " ({}{:.2f} {})".format( 880 "+" if bondChangesDelta > 0 else "", 881 bondChangesDelta, 882 aciCurrency 883 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 884 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 885 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 886 currency 887 ), 888 ) 889 ), 890 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 891 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 892 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 893 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 894 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 895 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 896 )), 897 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 898 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 899 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 900 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 901 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 902 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 903 )), 904 ]) 905 906 if "lot" in iJSON.keys(): 907 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 908 909 if "step" in iJSON.keys() and iJSON["step"] != 0: 910 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 911 912 # Add bond payment calendar: 913 if iJSON["type"] == "Bonds": 914 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 915 info.extend(["\n", strCalendar]) 916 917 infoText += "".join(info) 918 919 if show: 920 uLogger.info("{}".format(infoText)) 921 922 else: 923 uLogger.debug("{}".format(infoText)) 924 925 if self.infoFile is not None: 926 with open(self.infoFile, "w", encoding="UTF-8") as fH: 927 fH.write(infoText) 928 929 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 930 931 return infoText 932 933 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 934 """ 935 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 936 937 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 938 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 939 :return: JSON formatted data with information about instrument. 940 """ 941 tickerJSON = {} 942 if self.moreDebug: 943 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 944 945 if not self.ticker: 946 uLogger.warning("self.ticker variable is not be empty!") 947 948 else: 949 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 950 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 951 raise Exception("Instrument not allowed") 952 953 if not self.iList: 954 self.iList = self.Listing() 955 956 if self.ticker in self.iList["Shares"].keys(): 957 tickerJSON = self.iList["Shares"][self.ticker] 958 if self.moreDebug: 959 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 960 961 elif self.ticker in self.iList["Currencies"].keys(): 962 tickerJSON = self.iList["Currencies"][self.ticker] 963 if self.moreDebug: 964 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 965 966 elif self.ticker in self.iList["Bonds"].keys(): 967 tickerJSON = self.iList["Bonds"][self.ticker] 968 if self.moreDebug: 969 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Etfs"].keys(): 972 tickerJSON = self.iList["Etfs"][self.ticker] 973 if self.moreDebug: 974 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Futures"].keys(): 977 tickerJSON = self.iList["Futures"][self.ticker] 978 if self.moreDebug: 979 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 980 981 if tickerJSON: 982 self.figi = tickerJSON["figi"] 983 984 if requestPrice: 985 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 986 987 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 988 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 989 990 else: 991 tickerJSON["currentPrice"]["changes"] = 0 992 993 if show: 994 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 995 996 else: 997 if show: 998 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 999 1000 return tickerJSON 1001 1002 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1003 """ 1004 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1005 1006 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1007 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1008 :return: JSON formatted data with information about instrument. 1009 """ 1010 figiJSON = {} 1011 if self.moreDebug: 1012 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1013 1014 if not self.figi: 1015 uLogger.warning("self.figi variable is not be empty!") 1016 1017 else: 1018 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1019 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1020 raise Exception("Instrument not allowed") 1021 1022 if not self.iList: 1023 self.iList = self.Listing() 1024 1025 for item in self.iList["Shares"].keys(): 1026 if self.figi == self.iList["Shares"][item]["figi"]: 1027 figiJSON = self.iList["Shares"][item] 1028 1029 if self.moreDebug: 1030 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1031 1032 break 1033 1034 if not figiJSON: 1035 for item in self.iList["Currencies"].keys(): 1036 if self.figi == self.iList["Currencies"][item]["figi"]: 1037 figiJSON = self.iList["Currencies"][item] 1038 1039 if self.moreDebug: 1040 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1041 1042 break 1043 1044 if not figiJSON: 1045 for item in self.iList["Bonds"].keys(): 1046 if self.figi == self.iList["Bonds"][item]["figi"]: 1047 figiJSON = self.iList["Bonds"][item] 1048 1049 if self.moreDebug: 1050 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1051 1052 break 1053 1054 if not figiJSON: 1055 for item in self.iList["Etfs"].keys(): 1056 if self.figi == self.iList["Etfs"][item]["figi"]: 1057 figiJSON = self.iList["Etfs"][item] 1058 1059 if self.moreDebug: 1060 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1061 1062 break 1063 1064 if not figiJSON: 1065 for item in self.iList["Futures"].keys(): 1066 if self.figi == self.iList["Futures"][item]["figi"]: 1067 figiJSON = self.iList["Futures"][item] 1068 1069 if self.moreDebug: 1070 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1071 1072 break 1073 1074 if figiJSON: 1075 self.figi = figiJSON["figi"] 1076 self.ticker = figiJSON["ticker"] 1077 1078 if requestPrice: 1079 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1080 1081 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1082 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1083 1084 else: 1085 figiJSON["currentPrice"]["changes"] = 0 1086 1087 if show: 1088 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1089 1090 else: 1091 if show: 1092 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1093 1094 return figiJSON 1095 1096 def GetCurrentPrices(self, show: bool = True) -> dict: 1097 """ 1098 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1099 `{"buy": [{"price": 1243.8, "quantity": 193}, 1100 {"price": 1244.0, "quantity": 168}, 1101 {"price": 1244.8, "quantity": 5}, 1102 {"price": 1245.0, "quantity": 61}, 1103 {"price": 1245.4, "quantity": 60}], 1104 "sell": [{"price": 1243.6, "quantity": 8}, 1105 {"price": 1242.6, "quantity": 10}, 1106 {"price": 1242.4, "quantity": 18}, 1107 {"price": 1242.2, "quantity": 50}, 1108 {"price": 1242.0, "quantity": 113}], 1109 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1110 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1111 - sell: list of dicts with Buyers prices, 1112 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1113 - quantity: volume value by current price in lots, 1114 - limitUp: current trade session limit price, maximum, 1115 - limitDown: current trade session limit price, minimum, 1116 - lastPrice: last deal price of the instrument, 1117 - closePrice: previous trade session close price of the instrument. 1118 1119 See also: `SearchByTicker()` and `SearchByFIGI()`. 1120 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1121 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1122 1123 :param show: if `True` then print DOM to log and console. 1124 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1125 If an error occurred then returns an empty record: 1126 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1127 """ 1128 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1129 1130 if self.depth < 1: 1131 uLogger.error("Depth of Market (DOM) must be >=1!") 1132 raise Exception("Incorrect value") 1133 1134 if not (self.ticker or self.figi): 1135 uLogger.error("self.ticker or self.figi variables must be defined!") 1136 raise Exception("Ticker or FIGI required") 1137 1138 if self.ticker and not self.figi: 1139 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1140 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1141 1142 if not self.ticker and self.figi: 1143 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1144 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1145 1146 if not self.figi: 1147 uLogger.error("FIGI is not defined!") 1148 raise Exception("Ticker or FIGI required") 1149 1150 else: 1151 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1152 1153 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1154 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1155 self.body = str({"figi": self.figi, "depth": self.depth}) 1156 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1157 1158 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1159 # list of dicts with sellers orders: 1160 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1161 1162 # list of dicts with buyers orders: 1163 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1164 1165 # max price of instrument at this time: 1166 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1167 1168 # min price of instrument at this time: 1169 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1170 1171 # last price of deal with instrument: 1172 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1173 1174 # last close price of instrument: 1175 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1176 1177 else: 1178 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1179 uLogger.debug("Server response: {}".format(pricesResponse)) 1180 1181 if show: 1182 if prices["buy"] or prices["sell"]: 1183 info = [ 1184 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1185 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1186 self.ticker, 1187 self.figi, 1188 self.depth, 1189 ), 1190 "-" * 60, "\n", 1191 " Orders of Buyers | Orders of Sellers\n", 1192 "-" * 60, "\n", 1193 " Sell prices (volumes) | Buy prices (volumes)\n", 1194 "-" * 60, "\n", 1195 ] 1196 1197 if not prices["buy"]: 1198 info.append(" | No orders!\n") 1199 sumBuy = 0 1200 1201 else: 1202 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1203 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1204 for item in maxMinSorted: 1205 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1206 1207 if not prices["sell"]: 1208 info.append("No orders! |\n") 1209 sumSell = 0 1210 1211 else: 1212 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1213 for item in prices["sell"]: 1214 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1215 1216 info.extend([ 1217 "-" * 60, "\n", 1218 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1219 "-" * 60, "\n", 1220 ]) 1221 1222 infoText = "".join(info) 1223 1224 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1225 1226 else: 1227 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1228 1229 return prices 1230 1231 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1232 """ 1233 This method get and show information about all available broker instruments for current user account. 1234 If `instrumentsFile` string is not empty then also save information to this file. 1235 1236 :param show: if `True` then print results to console, if `False` - print only to file. 1237 :return: multi-lines string with all available broker instruments 1238 """ 1239 if not self.iList: 1240 self.iList = self.Listing() 1241 1242 info = [ 1243 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1244 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1245 ] 1246 1247 # add instruments count by type: 1248 for iType in self.iList.keys(): 1249 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1250 1251 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1252 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1253 1254 # generating info tables with all instruments by type: 1255 for iType in self.iList.keys(): 1256 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1257 1258 for instrument in self.iList[iType].keys(): 1259 iName = self.iList[iType][instrument]["name"] # instrument's name 1260 if len(iName) > 57: 1261 iName = "{}...".format(iName[:54]) # right trim for a long string 1262 1263 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1264 self.iList[iType][instrument]["ticker"], 1265 iName, 1266 self.iList[iType][instrument]["figi"], 1267 self.iList[iType][instrument]["currency"], 1268 self.iList[iType][instrument]["lot"], 1269 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1270 )) 1271 1272 infoText = "".join(info) 1273 1274 if show: 1275 uLogger.info(infoText) 1276 1277 if self.instrumentsFile: 1278 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1279 fH.write(infoText) 1280 1281 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1282 1283 return infoText 1284 1285 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1286 """ 1287 This method search and show information about instruments by part of its ticker, FIGI or name. 1288 If `searchResultsFile` string is not empty then also save information to this file. 1289 1290 :param pattern: string with part of ticker, FIGI or instrument's name. 1291 :param show: if `True` then print results to console, if `False` - return list of result only. 1292 :return: list of dictionaries with all found instruments. 1293 """ 1294 if not self.iList: 1295 self.iList = self.Listing() 1296 1297 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1298 compiledPattern = re.compile(pattern, re.IGNORECASE) 1299 1300 for iType in self.iList: 1301 for instrument in self.iList[iType].values(): 1302 searchResult = compiledPattern.search(" ".join( 1303 [instrument["ticker"], instrument["figi"], instrument["name"]] 1304 )) 1305 1306 if searchResult: 1307 searchResults[iType][instrument["ticker"]] = instrument 1308 1309 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1310 info = [ 1311 "# Search results\n\n", 1312 "* **Search pattern:** [{}]\n".format(pattern), 1313 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1314 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1315 ] 1316 infoShort = info[:] 1317 1318 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1319 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1320 skippedLine = "| ... | ... | ... | ... |\n" 1321 1322 if resultsLen == 0: 1323 info.append("\nNo results\n") 1324 infoShort.append("\nNo results\n") 1325 uLogger.warning("No results. Try changing your search pattern.") 1326 1327 else: 1328 for iType in searchResults: 1329 iTypeValuesCount = len(searchResults[iType].values()) 1330 if iTypeValuesCount > 0: 1331 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1332 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1333 1334 for instrument in searchResults[iType].values(): 1335 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1336 instrument["type"], 1337 instrument["ticker"], 1338 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1339 instrument["figi"], 1340 )) 1341 1342 if iTypeValuesCount <= 5: 1343 infoShort.extend(info[-iTypeValuesCount:]) 1344 1345 else: 1346 infoShort.extend(info[-5:]) 1347 infoShort.append(skippedLine) 1348 1349 infoText = "".join(info) 1350 infoTextShort = "".join(infoShort) 1351 1352 if show: 1353 uLogger.info(infoTextShort) 1354 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1355 1356 if self.searchResultsFile: 1357 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1358 fH.write(infoText) 1359 1360 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1361 1362 return searchResults 1363 1364 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1365 """ 1366 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1367 1368 :param instruments: list of strings with tickers or FIGIs. 1369 :return: list with unique instrument FIGIs only. 1370 """ 1371 requestedInstruments = [] 1372 for iName in instruments: 1373 if iName not in self.aliases.keys(): 1374 if iName not in requestedInstruments: 1375 requestedInstruments.append(iName) 1376 1377 else: 1378 if iName not in requestedInstruments: 1379 if self.aliases[iName] not in requestedInstruments: 1380 requestedInstruments.append(self.aliases[iName]) 1381 1382 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1383 1384 onlyUniqueFIGIs = [] 1385 for iName in requestedInstruments: 1386 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1387 continue 1388 1389 self.ticker = iName 1390 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1391 1392 if not iData: 1393 self.ticker = "" 1394 self.figi = iName 1395 1396 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1397 1398 if not iData: 1399 self.figi = "" 1400 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1401 1402 if iData and iData["figi"] not in onlyUniqueFIGIs: 1403 onlyUniqueFIGIs.append(iData["figi"]) 1404 1405 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1406 1407 return onlyUniqueFIGIs 1408 1409 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1410 """ 1411 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1412 See limits: https://tinkoff.github.io/investAPI/limits/ 1413 If `pricesFile` string is not empty then also save information to this file. 1414 1415 :param instruments: list of strings with tickers or FIGIs. 1416 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1417 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1418 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1419 """ 1420 if instruments is None or not instruments: 1421 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1422 raise Exception("Ticker or FIGI required") 1423 1424 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1425 1426 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1427 1428 iList = [] # trying to get info and current prices about all unique instruments: 1429 for self.figi in onlyUniqueFIGIs: 1430 iData = self.SearchByFIGI(requestPrice=True) 1431 iList.append(iData) 1432 1433 self.ShowListOfPrices(iList, show) 1434 1435 return iList 1436 1437 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1438 """ 1439 Show table contains current prices of given instruments. 1440 1441 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1442 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1443 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1444 :return: multilines text in Markdown format as a table contains current prices. 1445 """ 1446 infoText = "" 1447 1448 if show or self.pricesFile: 1449 info = [ 1450 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1451 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1452 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1453 ] 1454 1455 for item in iList: 1456 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1457 item["ticker"], 1458 item["figi"], 1459 item["type"], 1460 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1461 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1462 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1463 "{} / {}".format( 1464 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1465 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1466 ), 1467 "{} / {}".format( 1468 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1469 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1470 ), 1471 item["currency"], 1472 )) 1473 1474 infoText = "".join(info) 1475 1476 if show: 1477 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1478 1479 if self.pricesFile: 1480 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1481 fH.write(infoText) 1482 1483 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1484 1485 return infoText 1486 1487 def RequestTradingStatus(self) -> dict: 1488 """ 1489 Requesting trading status for the instrument defined by `figi` variable. 1490 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1491 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1492 1493 :return: dictionary with trading status attributes. Response example: 1494 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1495 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1496 """ 1497 if self.figi is None or not self.figi: 1498 uLogger.error("Variable `figi` must be defined for using this method!") 1499 raise Exception("FIGI required") 1500 1501 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1502 1503 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1504 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1505 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1506 1507 if self.moreDebug: 1508 uLogger.debug("Records about current trading status successfully received") 1509 1510 return tradingStatus 1511 1512 def RequestPortfolio(self) -> dict: 1513 """ 1514 Requesting actual user's portfolio for current `accountId`. 1515 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1516 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1517 1518 :return: dictionary with user's portfolio. 1519 """ 1520 if self.accountId is None or not self.accountId: 1521 uLogger.error("Variable `accountId` must be defined for using this method!") 1522 raise Exception("Account ID required") 1523 1524 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1525 1526 self.body = str({"accountId": self.accountId}) 1527 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1528 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1529 1530 if self.moreDebug: 1531 uLogger.debug("Records about user's portfolio successfully received") 1532 1533 return rawPortfolio 1534 1535 def RequestPositions(self) -> dict: 1536 """ 1537 Requesting open positions by currencies and instruments for current `accountId`. 1538 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1539 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1540 1541 :return: dictionary with open positions by instruments. 1542 """ 1543 if self.accountId is None or not self.accountId: 1544 uLogger.error("Variable `accountId` must be defined for using this method!") 1545 raise Exception("Account ID required") 1546 1547 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1548 1549 self.body = str({"accountId": self.accountId}) 1550 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1551 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1552 1553 if self.moreDebug: 1554 uLogger.debug("Records about current open positions successfully received") 1555 1556 return rawPositions 1557 1558 def RequestPendingOrders(self) -> list: 1559 """ 1560 Requesting current actual pending orders for current `accountId`. 1561 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1562 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1563 1564 :return: list of dictionaries with pending orders. 1565 """ 1566 if self.accountId is None or not self.accountId: 1567 uLogger.error("Variable `accountId` must be defined for using this method!") 1568 raise Exception("Account ID required") 1569 1570 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1571 1572 self.body = str({"accountId": self.accountId}) 1573 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1574 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1575 1576 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1577 1578 return rawOrders 1579 1580 def RequestStopOrders(self) -> list: 1581 """ 1582 Requesting current actual stop orders for current `accountId`. 1583 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1584 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1585 1586 :return: list of dictionaries with stop orders. 1587 """ 1588 if self.accountId is None or not self.accountId: 1589 uLogger.error("Variable `accountId` must be defined for using this method!") 1590 raise Exception("Account ID required") 1591 1592 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1593 1594 self.body = str({"accountId": self.accountId}) 1595 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1596 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1597 1598 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1599 1600 return rawStopOrders 1601 1602 def Overview(self, show: bool = False, details: str = "full") -> dict: 1603 """ 1604 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1605 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1606 are defined then also save information to file. 1607 1608 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1609 many requests about the state of the portfolio, and then, based on the received data, a large number 1610 of calculation and statistics are collected. 1611 1612 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1613 :param details: how detailed should the information be? You should specify one of strings: 1614 `full` - shows full available information about portfolio status (by default), 1615 `positions` - shows only open positions, 1616 `digest` - show a short digest of the portfolio status, 1617 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1618 `orders` - shows only sections of open limits and stop orders. 1619 :return: dictionary with client's raw portfolio and some statistics. 1620 """ 1621 if self.accountId is None or not self.accountId: 1622 uLogger.error("Variable `accountId` must be defined for using this method!") 1623 raise Exception("Account ID required") 1624 1625 view = { 1626 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1627 "headers": {}, # list of dictionaries, response headers without "positions" section 1628 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1629 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1630 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1631 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1632 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1633 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1634 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1635 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1636 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1637 }, 1638 "stat": { # --- some statistics calculated using "raw" sections: 1639 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1640 "availableRUB": 0., # available rubles (without other currencies) 1641 "blockedRUB": 0., # blocked sum in Russian Rouble 1642 "totalChangesRUB": 0., # changes for all open trades in RUB 1643 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1644 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1645 "sharesCostRUB": 0., # costs of all shares in RUB 1646 "bondsCostRUB": 0., # costs of all bonds in RUB 1647 "etfsCostRUB": 0., # costs of all etfs in RUB 1648 "futuresCostRUB": 0., # costs of all futures in RUB 1649 "Currencies": [], # list of dictionaries of all currencies statistics 1650 "Shares": [], # list of dictionaries of all shares statistics 1651 "Bonds": [], # list of dictionaries of all bonds statistics 1652 "Etfs": [], # list of dictionaries of all etfs statistics 1653 "Futures": [], # list of dictionaries of all futures statistics 1654 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1655 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1656 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1657 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1658 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1659 }, 1660 "analytics": { # --- some analytics of portfolio: 1661 "distrByAssets": {}, # portfolio distribution by assets 1662 "distrByCompanies": {}, # portfolio distribution by companies 1663 "distrBySectors": {}, # portfolio distribution by sectors 1664 "distrByCurrencies": {}, # portfolio distribution by currencies 1665 "distrByCountries": {}, # portfolio distribution by countries 1666 } 1667 } 1668 1669 details = details.lower() 1670 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1671 if details not in availableDetails: 1672 details = "full" 1673 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1674 1675 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1676 1677 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1678 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1679 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1680 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1681 1682 # save response headers without "positions" section: 1683 for key in portfolioResponse.keys(): 1684 if key != "positions": 1685 view["raw"]["headers"][key] = portfolioResponse[key] 1686 1687 else: 1688 continue 1689 1690 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1691 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1692 for item in portfolioResponse["positions"]: 1693 if item["instrumentType"] == "currency": 1694 self.figi = item["figi"] 1695 curr = self.SearchByFIGI(requestPrice=False) 1696 1697 # current price of currency in RUB: 1698 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1699 "name": curr["name"], 1700 "currentPrice": NanoToFloat( 1701 item["currentPrice"]["units"], 1702 item["currentPrice"]["nano"] 1703 ), 1704 } 1705 1706 view["raw"]["Currencies"].append(item) 1707 1708 elif item["instrumentType"] == "share": 1709 view["raw"]["Shares"].append(item) 1710 1711 elif item["instrumentType"] == "bond": 1712 view["raw"]["Bonds"].append(item) 1713 1714 elif item["instrumentType"] == "etf": 1715 view["raw"]["Etfs"].append(item) 1716 1717 elif item["instrumentType"] == "futures": 1718 view["raw"]["Futures"].append(item) 1719 1720 else: 1721 continue 1722 1723 # how many volume of currencies (by ISO currency name) are blocked: 1724 for item in view["raw"]["positions"]["blocked"]: 1725 blocked = NanoToFloat(item["units"], item["nano"]) 1726 if blocked > 0: 1727 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1728 1729 # how many volume of instruments (by FIGI) are blocked: 1730 for item in view["raw"]["positions"]["securities"]: 1731 blocked = int(item["blocked"]) 1732 if blocked > 0: 1733 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1734 1735 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1736 1737 if "rub" in allBlocked.keys(): 1738 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1739 1740 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1741 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1742 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1743 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1744 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1745 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1746 view["stat"]["portfolioCostRUB"] = sum([ 1747 view["stat"]["allCurrenciesCostRUB"], 1748 view["stat"]["sharesCostRUB"], 1749 view["stat"]["bondsCostRUB"], 1750 view["stat"]["etfsCostRUB"], 1751 view["stat"]["futuresCostRUB"], 1752 ]) 1753 1754 # --- calculating some portfolio statistics: 1755 byComp = {} # distribution by companies 1756 bySect = {} # distribution by sectors 1757 byCurr = {} # distribution by currencies (include RUB) 1758 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1759 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1760 1761 for item in portfolioResponse["positions"]: 1762 self.figi = item["figi"] 1763 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1764 1765 if instrument: 1766 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1767 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1768 1769 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1770 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1771 1772 else: 1773 blocked = 0 1774 1775 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1776 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1777 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1778 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1779 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1780 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1781 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1782 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1783 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1784 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1785 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1786 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1787 1788 statData = { 1789 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1790 "ticker": instrument["ticker"], # ticker by FIGI 1791 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1792 "volume": volume, # available volume of instrument 1793 "lots": lots, # volume in lots of instrument 1794 "direction": direction, # direction of an instrument's position: short or long 1795 "blocked": blocked, # blocked volume of currency or instrument 1796 "currentPrice": curPrice, # current instrument's price in basic asset 1797 "average": average, # current average position price 1798 "cost": cost, # current cost of all volume of instrument in basic asset 1799 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1800 "costRUB": costRUB, # cost of instrument in ruble 1801 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1802 "profit": profit, # expected profit at current moment 1803 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1804 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1805 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1806 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1807 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1808 "step": instrument["step"], # minimum price increment 1809 } 1810 1811 # adding distribution by unique countries: 1812 if statData["country"] not in byCountry.keys(): 1813 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1814 1815 else: 1816 byCountry[statData["country"]]["cost"] += costRUB 1817 byCountry[statData["country"]]["percent"] += percentCostRUB 1818 1819 if item["instrumentType"] != "currency": 1820 # adding distribution by unique companies: 1821 if statData["name"]: 1822 if statData["name"] not in byComp.keys(): 1823 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1824 1825 else: 1826 byComp[statData["name"]]["cost"] += costRUB 1827 byComp[statData["name"]]["percent"] += percentCostRUB 1828 1829 # adding distribution by unique sectors: 1830 if statData["sector"] not in bySect.keys(): 1831 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1832 1833 else: 1834 bySect[statData["sector"]]["cost"] += costRUB 1835 bySect[statData["sector"]]["percent"] += percentCostRUB 1836 1837 # adding distribution by unique currencies: 1838 if currency not in byCurr.keys(): 1839 byCurr[currency] = { 1840 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1841 "cost": costRUB, 1842 "percent": percentCostRUB 1843 } 1844 1845 else: 1846 byCurr[currency]["cost"] += costRUB 1847 byCurr[currency]["percent"] += percentCostRUB 1848 1849 # saving statistics for every instrument: 1850 if item["instrumentType"] == "currency": 1851 view["stat"]["Currencies"].append(statData) 1852 1853 # update dict with free funds for trading (total - blocked) by currencies 1854 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1855 view["stat"]["funds"][currency] = { 1856 "total": volume, 1857 "totalCostRUB": costRUB, # total volume cost in rubles 1858 "free": volume - blocked, 1859 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1860 } 1861 1862 elif item["instrumentType"] == "share": 1863 view["stat"]["Shares"].append(statData) 1864 1865 elif item["instrumentType"] == "bond": 1866 view["stat"]["Bonds"].append(statData) 1867 1868 elif item["instrumentType"] == "etf": 1869 view["stat"]["Etfs"].append(statData) 1870 1871 elif item["instrumentType"] == "Futures": 1872 view["stat"]["Futures"].append(statData) 1873 1874 else: 1875 continue 1876 1877 # total changes in Russian Ruble: 1878 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1879 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1880 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1881 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1882 view["stat"]["funds"]["rub"] = { 1883 "total": view["stat"]["availableRUB"], 1884 "totalCostRUB": view["stat"]["availableRUB"], 1885 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1886 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1887 } 1888 1889 # --- pending orders sector data: 1890 uniquePendingOrders = [] 1891 uniquePendingOrdersFIGIs = [] 1892 for item in view["raw"]["orders"]: 1893 if item["figi"] not in uniquePendingOrdersFIGIs: 1894 uniquePendingOrdersFIGIs.append(item["figi"]) 1895 uniquePendingOrders.append(item) 1896 1897 for item in uniquePendingOrders: 1898 self.figi = item["figi"] 1899 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1900 1901 if instrument: 1902 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1903 orderType = TKS_ORDER_TYPES[item["orderType"]] 1904 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1905 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1906 1907 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1908 if item["direction"] == "ORDER_DIRECTION_BUY": 1909 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1910 1911 else: 1912 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1913 1914 # requested price for order execution: 1915 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1916 1917 # necessary changes in percent to reach target from current price: 1918 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1919 1920 view["stat"]["orders"].append({ 1921 "orderID": item["orderId"], # orderId number parameter of current order 1922 "figi": item["figi"], # FIGI identification 1923 "ticker": instrument["ticker"], # ticker name by FIGI 1924 "lotsRequested": item["lotsRequested"], # requested lots value 1925 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1926 "currentPrice": lastPrice, # current instrument's price for defined action 1927 "targetPrice": target, # requested price for order execution in base currency 1928 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1929 "percentChanges": changes, # changes in percent to target from current price 1930 "currency": item["currency"], # instrument's currency name 1931 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1932 "type": orderType, # type of order from TKS_ORDER_TYPES 1933 "status": orderState, # order status from TKS_ORDER_STATES 1934 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1935 }) 1936 1937 # --- stop orders sector data: 1938 uniqueStopOrders = [] 1939 uniqueStopOrdersFIGIs = [] 1940 for item in view["raw"]["stopOrders"]: 1941 if item["figi"] not in uniqueStopOrdersFIGIs: 1942 uniqueStopOrdersFIGIs.append(item["figi"]) 1943 uniqueStopOrders.append(item) 1944 1945 for item in uniqueStopOrders: 1946 self.figi = item["figi"] 1947 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1948 1949 if instrument: 1950 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1951 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1952 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1953 1954 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1955 if "expirationTime" in item.keys(): 1956 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1957 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1958 1959 else: 1960 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1961 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1962 1963 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1964 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1965 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1966 1967 else: 1968 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1969 1970 # requested price when stop-order executed: 1971 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1972 1973 # price for limit-order, set up when stop-order executed: 1974 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1975 1976 # necessary changes in percent to reach target from current price: 1977 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1978 1979 view["stat"]["stopOrders"].append({ 1980 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1981 "figi": item["figi"], # FIGI identification 1982 "ticker": instrument["ticker"], # ticker name by FIGI 1983 "lotsRequested": item["lotsRequested"], # requested lots value 1984 "currentPrice": lastPrice, # current instrument's price for defined action 1985 "targetPrice": target, # requested price for stop-order execution in base currency 1986 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1987 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1988 "percentChanges": changes, # changes in percent to target from current price 1989 "currency": item["currency"], # instrument's currency name 1990 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1991 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1992 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1993 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1994 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1995 }) 1996 1997 # --- calculating data for analytics section: 1998 # portfolio distribution by assets: 1999 view["analytics"]["distrByAssets"] = { 2000 "Ruble": { 2001 "uniques": 1, 2002 "cost": view["stat"]["availableRUB"], 2003 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2004 }, 2005 "Currencies": { 2006 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2007 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2008 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Shares": { 2011 "uniques": len(view["stat"]["Shares"]), 2012 "cost": view["stat"]["sharesCostRUB"], 2013 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Bonds": { 2016 "uniques": len(view["stat"]["Bonds"]), 2017 "cost": view["stat"]["bondsCostRUB"], 2018 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Etfs": { 2021 "uniques": len(view["stat"]["Etfs"]), 2022 "cost": view["stat"]["etfsCostRUB"], 2023 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Futures": { 2026 "uniques": len(view["stat"]["Futures"]), 2027 "cost": view["stat"]["futuresCostRUB"], 2028 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 } 2031 2032 # portfolio distribution by companies: 2033 view["analytics"]["distrByCompanies"]["All money cash"] = { 2034 "ticker": "", 2035 "cost": view["stat"]["allCurrenciesCostRUB"], 2036 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2037 } 2038 view["analytics"]["distrByCompanies"].update(byComp) 2039 2040 # portfolio distribution by sectors: 2041 view["analytics"]["distrBySectors"]["All money cash"] = { 2042 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2043 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2044 } 2045 view["analytics"]["distrBySectors"].update(bySect) 2046 2047 # portfolio distribution by currencies: 2048 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2049 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2050 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2051 2052 view["analytics"]["distrByCurrencies"].update(byCurr) 2053 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2054 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2055 2056 # portfolio distribution by countries: 2057 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2058 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2059 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2060 2061 view["analytics"]["distrByCountries"].update(byCountry) 2062 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2063 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2064 2065 # --- Prepare text statistics overview in human-readable: 2066 if show: 2067 # Whatever the value `details`, header not changes: 2068 info = [ 2069 "# Client's portfolio\n\n", 2070 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2071 "* **Account ID:** [{}]\n".format(self.accountId), 2072 ] 2073 2074 if details in ["full", "positions", "digest"]: 2075 info.extend([ 2076 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2077 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2078 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2079 view["stat"]["totalChangesRUB"], 2080 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2081 view["stat"]["totalChangesPercentRUB"], 2082 ), 2083 ]) 2084 2085 if details in ["full", "positions"]: 2086 info.extend([ 2087 "## Open positions\n\n", 2088 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2089 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2090 "| Ruble | {:>31} | | | | | |\n".format( 2091 "{:.2f} ({:.2f}) rub".format( 2092 view["stat"]["availableRUB"], 2093 view["stat"]["blockedRUB"], 2094 ) 2095 ) 2096 ]) 2097 2098 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2099 return [ 2100 "| | | | | | | |\n", 2101 "| {:<27} | | | | | {:>19} | |\n".format( 2102 noTradeStr if noTradeStr else typeStr, 2103 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2104 ), 2105 ] 2106 2107 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2108 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2109 "{} [{}]".format(data["ticker"], data["figi"]), 2110 "{:.2f} ({:.2f}) {}".format( 2111 data["volume"], 2112 data["blocked"], 2113 data["currency"], 2114 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2115 data["volume"], 2116 data["blocked"], 2117 ), 2118 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2119 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2120 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2121 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2122 "{}{:.2f} {} ({}{:.2f}%)".format( 2123 "+" if data["profit"] > 0 else "", 2124 data["profit"], data["baseCurrencyName"], 2125 "+" if data["percentProfit"] > 0 else "", 2126 data["percentProfit"], 2127 ), 2128 ) 2129 2130 # --- Show currencies section: 2131 if view["stat"]["Currencies"]: 2132 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2133 for item in view["stat"]["Currencies"]: 2134 info.append(_InfoStr(item, showCurrencyName=True)) 2135 2136 else: 2137 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2138 2139 # --- Show shares section: 2140 if view["stat"]["Shares"]: 2141 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2142 2143 for item in view["stat"]["Shares"]: 2144 info.append(_InfoStr(item)) 2145 2146 else: 2147 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2148 2149 # --- Show bonds section: 2150 if view["stat"]["Bonds"]: 2151 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2152 2153 for item in view["stat"]["Bonds"]: 2154 info.append(_InfoStr(item)) 2155 2156 else: 2157 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2158 2159 # --- Show etfs section: 2160 if view["stat"]["Etfs"]: 2161 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2162 2163 for item in view["stat"]["Etfs"]: 2164 info.append(_InfoStr(item)) 2165 2166 else: 2167 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2168 2169 # --- Show futures section: 2170 if view["stat"]["Futures"]: 2171 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2172 2173 for item in view["stat"]["Futures"]: 2174 info.append(_InfoStr(item)) 2175 2176 else: 2177 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2178 2179 if details in ["full", "orders"]: 2180 # --- Show pending orders section: 2181 if view["stat"]["orders"]: 2182 info.extend([ 2183 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2184 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2185 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2186 ]) 2187 2188 for item in view["stat"]["orders"]: 2189 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2190 "{} [{}]".format(item["ticker"], item["figi"]), 2191 item["orderID"], 2192 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2193 "{} {} ({}{:.2f}%)".format( 2194 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2195 item["baseCurrencyName"], 2196 "+" if item["percentChanges"] > 0 else "", 2197 float(item["percentChanges"]), 2198 ), 2199 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2200 item["action"], 2201 item["type"], 2202 item["date"], 2203 )) 2204 2205 else: 2206 info.append("\n## Total pending limit-orders: 0\n") 2207 2208 # --- Show stop orders section: 2209 if view["stat"]["stopOrders"]: 2210 info.extend([ 2211 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2212 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2213 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2214 ]) 2215 2216 for item in view["stat"]["stopOrders"]: 2217 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2218 "{} [{}]".format(item["ticker"], item["figi"]), 2219 item["orderID"], 2220 item["lotsRequested"], 2221 "{} {} ({}{:.2f}%)".format( 2222 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2223 item["baseCurrencyName"], 2224 "+" if item["percentChanges"] > 0 else "", 2225 float(item["percentChanges"]), 2226 ), 2227 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2228 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2229 item["action"], 2230 item["type"], 2231 item["expType"], 2232 item["createDate"], 2233 item["expDate"], 2234 )) 2235 2236 else: 2237 info.append("\n## Total stop-orders: 0\n") 2238 2239 if details in ["full", "analytics"]: 2240 # -- Show analytics section: 2241 if view["stat"]["portfolioCostRUB"] > 0: 2242 info.extend([ 2243 "\n# Analytics\n" 2244 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2245 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2246 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2247 view["stat"]["totalChangesRUB"], 2248 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2249 view["stat"]["totalChangesPercentRUB"], 2250 ), 2251 "\n## Portfolio distribution by assets\n" 2252 "\n| Type | Uniques | Percent | Current cost |\n", 2253 "|------------|---------|---------|--------------------|\n", 2254 ]) 2255 2256 for key in view["analytics"]["distrByAssets"].keys(): 2257 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2258 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2259 key, 2260 view["analytics"]["distrByAssets"][key]["uniques"], 2261 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2262 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2263 )) 2264 2265 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2266 info.extend([ 2267 "\n## Portfolio distribution by companies\n" 2268 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2269 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2270 ]) 2271 2272 for company in view["analytics"]["distrByCompanies"].keys(): 2273 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2274 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2275 info.append("| {} | {:<7} | {:<18} |\n".format( 2276 "{}{}{}".format( 2277 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2278 company, 2279 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2280 ), 2281 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2282 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2283 )) 2284 2285 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2286 info.extend([ 2287 "\n## Portfolio distribution by sectors\n" 2288 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2289 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2290 ]) 2291 2292 for sector in view["analytics"]["distrBySectors"].keys(): 2293 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2294 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2295 sector, 2296 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2297 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2298 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2299 )) 2300 2301 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2302 info.extend([ 2303 "\n## Portfolio distribution by currencies\n" 2304 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2305 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2306 ]) 2307 2308 for curr in view["analytics"]["distrByCurrencies"].keys(): 2309 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2310 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2311 info.append("| {} | {:<7} | {:<18} |\n".format( 2312 "[{}] {}{}".format( 2313 curr, 2314 view["analytics"]["distrByCurrencies"][curr]["name"], 2315 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2316 ), 2317 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2318 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2319 )) 2320 2321 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2322 info.extend([ 2323 "\n## Portfolio distribution by countries\n" 2324 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2325 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2326 ]) 2327 2328 for country in view["analytics"]["distrByCountries"].keys(): 2329 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2330 nameLen = len(country) 2331 info.append("| {} | {:<7} | {:<18} |\n".format( 2332 "{}{}".format( 2333 country, 2334 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2335 ), 2336 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2337 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2338 )) 2339 2340 infoText = "".join(info) 2341 2342 uLogger.info(infoText) 2343 2344 if details == "full" and self.overviewFile: 2345 filename = self.overviewFile 2346 2347 elif details == "digest" and self.overviewDigestFile: 2348 filename = self.overviewDigestFile 2349 2350 elif details == "positions" and self.overviewPositionsFile: 2351 filename = self.overviewPositionsFile 2352 2353 elif details == "orders" and self.overviewOrdersFile: 2354 filename = self.overviewOrdersFile 2355 2356 elif details == "analytics" and self.overviewAnalyticsFile: 2357 filename = self.overviewAnalyticsFile 2358 2359 else: 2360 filename = "" 2361 2362 if filename: 2363 with open(filename, "w", encoding="UTF-8") as fH: 2364 fH.write(infoText) 2365 2366 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2367 2368 return view 2369 2370 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2371 """ 2372 Returns history operations between two given dates for current `accountId`. 2373 If `reportFile` string is not empty then also save human-readable report. 2374 Shows some statistical data of closed positions. 2375 2376 :param start: see docstring in `GetDatesAsString()` method 2377 :param end: see docstring in `GetDatesAsString()` method 2378 :param show: if `True` then also prints all records to the console. 2379 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2380 :return: original list of dictionaries with history of deals records from API ("operations" key): 2381 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2382 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2383 """ 2384 if self.accountId is None or not self.accountId: 2385 uLogger.error("Variable `accountId` must be defined for using this method!") 2386 raise Exception("Account ID required") 2387 2388 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2389 2390 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2391 2392 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2393 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2394 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2395 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2396 customStat = {} # custom statistics in additional to responseJSON 2397 2398 # --- output report in human-readable format: 2399 if show or self.reportFile: 2400 splitLine1 = "| | | | | |\n" # Summary section 2401 splitLine2 = "| | | | | | | | |\n" # Operations section 2402 nextDay = "" 2403 2404 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2405 2406 if len(ops) > 0: 2407 customStat = { 2408 "opsCount": 0, # total operations count 2409 "buyCount": 0, # buy operations 2410 "sellCount": 0, # sell operations 2411 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2412 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2413 "payIn": {"rub": 0.}, # Deposit brokerage account 2414 "payOut": {"rub": 0.}, # Withdrawals 2415 "divs": {"rub": 0.}, # Dividends income 2416 "coupons": {"rub": 0.}, # Coupon's income 2417 "brokerCom": {"rub": 0.}, # Service commissions 2418 "serviceCom": {"rub": 0.}, # Service commissions 2419 "marginCom": {"rub": 0.}, # Margin commissions 2420 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2421 } 2422 2423 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2424 for item in ops: 2425 if item["state"] == "OPERATION_STATE_EXECUTED": 2426 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2427 2428 # count buy operations: 2429 if "_BUY" in item["operationType"]: 2430 customStat["buyCount"] += 1 2431 2432 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2433 customStat["buyTotal"][item["payment"]["currency"]] += payment 2434 2435 else: 2436 customStat["buyTotal"][item["payment"]["currency"]] = payment 2437 2438 # count sell operations: 2439 elif "_SELL" in item["operationType"]: 2440 customStat["sellCount"] += 1 2441 2442 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2443 customStat["sellTotal"][item["payment"]["currency"]] += payment 2444 2445 else: 2446 customStat["sellTotal"][item["payment"]["currency"]] = payment 2447 2448 # count incoming operations: 2449 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2450 if item["payment"]["currency"] in customStat["payIn"].keys(): 2451 customStat["payIn"][item["payment"]["currency"]] += payment 2452 2453 else: 2454 customStat["payIn"][item["payment"]["currency"]] = payment 2455 2456 # count withdrawals operations: 2457 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2458 if item["payment"]["currency"] in customStat["payOut"].keys(): 2459 customStat["payOut"][item["payment"]["currency"]] += payment 2460 2461 else: 2462 customStat["payOut"][item["payment"]["currency"]] = payment 2463 2464 # count dividends income: 2465 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2466 if item["payment"]["currency"] in customStat["divs"].keys(): 2467 customStat["divs"][item["payment"]["currency"]] += payment 2468 2469 else: 2470 customStat["divs"][item["payment"]["currency"]] = payment 2471 2472 # count coupon's income: 2473 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2474 if item["payment"]["currency"] in customStat["coupons"].keys(): 2475 customStat["coupons"][item["payment"]["currency"]] += payment 2476 2477 else: 2478 customStat["coupons"][item["payment"]["currency"]] = payment 2479 2480 # count broker commissions: 2481 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2482 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2483 customStat["brokerCom"][item["payment"]["currency"]] += payment 2484 2485 else: 2486 customStat["brokerCom"][item["payment"]["currency"]] = payment 2487 2488 # count service commissions: 2489 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2490 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2491 customStat["serviceCom"][item["payment"]["currency"]] += payment 2492 2493 else: 2494 customStat["serviceCom"][item["payment"]["currency"]] = payment 2495 2496 # count margin commissions: 2497 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2498 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2499 customStat["marginCom"][item["payment"]["currency"]] += payment 2500 2501 else: 2502 customStat["marginCom"][item["payment"]["currency"]] = payment 2503 2504 # count withholding taxes: 2505 elif "_TAX" in item["operationType"]: 2506 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2507 customStat["allTaxes"][item["payment"]["currency"]] += payment 2508 2509 else: 2510 customStat["allTaxes"][item["payment"]["currency"]] = payment 2511 2512 else: 2513 continue 2514 2515 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2516 2517 # --- view "Actions" lines: 2518 info.extend([ 2519 "| Report sections | | | | |\n", 2520 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2521 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2522 "| | Buy: {:<22} | {:<28} | | |\n".format( 2523 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2524 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2525 ), 2526 "| | Sell: {:<21} | {:<28} | | |\n".format( 2527 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2528 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2529 ), 2530 ]) 2531 2532 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2533 for key in opsKeys: 2534 if key == "rub": 2535 continue 2536 2537 info.extend([ 2538 "| | | {:<28} | | |\n".format( 2539 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2540 ), 2541 "| | | {:<28} | | |\n".format( 2542 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2543 ), 2544 ]) 2545 2546 info.append(splitLine1) 2547 2548 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2549 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2550 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2551 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2552 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2553 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2554 ) 2555 2556 # --- view "Payments" lines: 2557 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2558 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2559 2560 for key in paymentsKeys: 2561 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2562 2563 info.append(splitLine1) 2564 2565 # --- view "Commissions and taxes" lines: 2566 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2567 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2568 2569 for key in comKeys: 2570 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2571 2572 info.append(splitLine1) 2573 2574 info.extend([ 2575 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2576 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2577 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2578 ]) 2579 2580 else: 2581 info.append("Broker returned no operations during this period\n") 2582 2583 # --- view "Operations" section: 2584 for item in ops: 2585 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2586 continue 2587 2588 else: 2589 self.figi = item["figi"] if item["figi"] else "" 2590 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2591 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2592 2593 # group of deals during one day: 2594 if nextDay and item["date"].split("T")[0] != nextDay: 2595 info.append(splitLine2) 2596 nextDay = "" 2597 2598 else: 2599 nextDay = item["date"].split("T")[0] # saving current day for splitting 2600 2601 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2602 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2603 self.figi if self.figi else "—", 2604 instrument["ticker"] if instrument else "—", 2605 instrument["type"] if instrument else "—", 2606 item["quantity"] if int(item["quantity"]) > 0 else "—", 2607 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2608 TKS_OPERATION_STATES[item["state"]], 2609 TKS_OPERATION_TYPES[item["operationType"]], 2610 )) 2611 2612 infoText = "".join(info) 2613 2614 if show: 2615 if self.moreDebug: 2616 uLogger.debug("Records about history of a client's operations successfully received") 2617 2618 uLogger.info(infoText) 2619 2620 if self.reportFile: 2621 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2622 fH.write(infoText) 2623 2624 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2625 2626 return ops, customStat 2627 2628 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2629 """ 2630 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2631 2632 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2633 Warning! Broker server used ISO UTC time by default. 2634 2635 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2636 Also, `historyFile` used to update history with `onlyMissing` parameter. 2637 2638 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2639 2640 :param start: see docstring in `GetDatesAsString()` method. 2641 :param end: see docstring in `GetDatesAsString()` method. 2642 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2643 `"hour"`, `"day"`. Default: `"hour"`. 2644 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2645 False by default. Warning! History appends only from last candle to current time 2646 with always update last candle! 2647 :param csvSep: separator if csv-file is used, `,` by default. 2648 :param show: if `True` then also prints Pandas DataFrame to the console. 2649 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2650 `["date", "time", "open", "high", "low", "close", "volume"]`. 2651 """ 2652 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2653 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2654 history = None # empty pandas object for history 2655 2656 if interval not in TKS_CANDLE_INTERVALS.keys(): 2657 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2658 raise Exception("Incorrect value") 2659 2660 if not (self.ticker or self.figi): 2661 uLogger.error("Ticker or FIGI must be defined!") 2662 raise Exception("Ticker or FIGI required") 2663 2664 if self.ticker and not self.figi: 2665 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2666 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2667 2668 if self.figi and not self.ticker: 2669 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2670 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2671 2672 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2673 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2674 if interval.lower() != "day": 2675 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2676 2677 delta = dtEnd - dtStart # current UTC time minus last time in file 2678 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2679 2680 # calculate history length in candles: 2681 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2682 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2683 length += 1 # to avoid fraction time 2684 2685 # calculate data blocks count: 2686 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2687 2688 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2689 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2690 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2691 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2692 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2693 2694 tempOld = None # pandas object for old history, if --only-missing key present 2695 lastTime = None # datetime object of last old candle in file 2696 2697 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2698 uLogger.debug("--only-missing key present, add only last missing candles...") 2699 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2700 2701 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2702 2703 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2704 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2705 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2706 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2707 2708 # get last datetime object from last string in file or minus 1 delta if file is empty: 2709 if len(tempOld) > 0: 2710 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2711 2712 else: 2713 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2714 2715 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2716 2717 responseJSONs = [] # raw history blocks of data 2718 2719 blockEnd = dtEnd 2720 for item in range(blocks): 2721 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2722 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2723 2724 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2725 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2726 )) 2727 2728 if blockStart == blockEnd: 2729 uLogger.debug("Skipped this zero-length block...") 2730 2731 else: 2732 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2733 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2734 self.body = str({ 2735 "figi": self.figi, 2736 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2737 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2738 "interval": TKS_CANDLE_INTERVALS[interval][0] 2739 }) 2740 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2741 2742 if "code" in responseJSON.keys(): 2743 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2744 2745 else: 2746 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2747 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2748 2749 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2750 2751 blockEnd = blockStart 2752 2753 printCount = len(responseJSONs) # candles to show in console 2754 if responseJSONs: 2755 tempHistory = pd.DataFrame( 2756 data={ 2757 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2758 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2759 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2760 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2761 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2762 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2763 "volume": [int(item["volume"]) for item in responseJSONs], 2764 }, 2765 index=range(len(responseJSONs)), 2766 columns=["date", "time", "open", "high", "low", "close", "volume"], 2767 ) 2768 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2769 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2770 2771 # append only newest candles to old history if --only-missing key present: 2772 if onlyMissing and tempOld is not None and lastTime is not None: 2773 index = 0 # find start index in tempHistory data: 2774 2775 for i, item in tempHistory.iterrows(): 2776 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2777 2778 if curTime == lastTime: 2779 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2780 index = i 2781 printCount = index + 1 2782 break 2783 2784 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2785 2786 else: 2787 history = tempHistory # if no `--only-missing` key then load full data from server 2788 2789 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2790 2791 if history is not None and not history.empty: 2792 if show: 2793 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2794 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2795 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2796 )) 2797 2798 else: 2799 uLogger.warning("Received an empty candles history!") 2800 2801 if self.historyFile is not None: 2802 if history is not None and not history.empty: 2803 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2804 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2805 2806 else: 2807 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2808 2809 else: 2810 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2811 2812 return history 2813 2814 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2815 """ 2816 Load candles history from csv-file and return Pandas DataFrame object. 2817 2818 See also: `History()` and `ShowHistoryChart()` methods. 2819 2820 :param filePath: path to csv-file to open. 2821 """ 2822 loadedHistory = None # init candles data object 2823 2824 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2825 2826 if os.path.exists(filePath): 2827 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2828 2829 tfStr = self.priceModel.FormattedDelta( 2830 self.priceModel.timeframe, 2831 "{days} days {hours}h {minutes}m {seconds}s", 2832 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2833 self.priceModel.timeframe, 2834 "{hours}h {minutes}m {seconds}s", 2835 ) 2836 2837 if loadedHistory is not None and not loadedHistory.empty: 2838 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2839 len(loadedHistory), 2840 tfStr, 2841 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2842 ) 2843 2844 else: 2845 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2846 2847 else: 2848 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2849 2850 return loadedHistory 2851 2852 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2853 """ 2854 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2855 2856 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2857 Default: `index.html` (both for interact and non-interact candlesticks chart). 2858 2859 See also: `History()` and `LoadHistory()` methods. 2860 2861 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2862 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2863 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2864 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2866 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2867 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2868 """ 2869 if isinstance(candles, str): 2870 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2871 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2872 2873 elif isinstance(candles, pd.DataFrame): 2874 self.priceModel.prices = candles # set candles chain from variable 2875 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2876 2877 if "datetime" not in candles.columns: 2878 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2879 2880 else: 2881 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2882 raise Exception("Incorrect value") 2883 2884 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2885 2886 if interact: 2887 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2888 2889 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2890 2891 else: 2892 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2893 2894 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2895 2896 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2897 2898 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2899 """ 2900 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2901 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2902 2903 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2904 2905 :param operation: string "Buy" or "Sell". 2906 :param lots: volume, integer count of lots >= 1. 2907 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2908 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2909 :param expDate: string "Undefined" by default or local date in future, 2910 it is a string with format `%Y-%m-%d %H:%M:%S`. 2911 :return: JSON with response from broker server. 2912 """ 2913 if self.accountId is None or not self.accountId: 2914 uLogger.error("Variable `accountId` must be defined for using this method!") 2915 raise Exception("Account ID required") 2916 2917 if operation is None or not operation or operation not in ("Buy", "Sell"): 2918 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2919 raise Exception("Incorrect value") 2920 2921 if lots is None or lots < 1: 2922 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2923 lots = 1 2924 2925 if tp is None or tp < 0: 2926 tp = 0 2927 2928 if sl is None or sl < 0: 2929 sl = 0 2930 2931 if expDate is None or not expDate: 2932 expDate = "Undefined" 2933 2934 if not (self.ticker or self.figi): 2935 uLogger.error("Ticker or FIGI must be defined!") 2936 raise Exception("Ticker or FIGI required") 2937 2938 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2939 self.ticker = instrument["ticker"] 2940 self.figi = instrument["figi"] 2941 2942 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2943 2944 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2945 self.body = str({ 2946 "figi": self.figi, 2947 "quantity": str(lots), 2948 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2949 "accountId": str(self.accountId), 2950 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2951 }) 2952 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2953 2954 if "orderId" in response.keys(): 2955 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2956 operation, response["orderId"], 2957 self.ticker, self.figi, lots, 2958 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2959 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2960 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2961 )) 2962 2963 else: 2964 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2965 2966 if tp > 0: 2967 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2968 2969 if sl > 0: 2970 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2971 2972 return response 2973 2974 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2975 """ 2976 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2977 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2978 2979 See also: `Order()` and `Trade()` docstrings. 2980 2981 :param lots: volume, integer count of lots >= 1. 2982 :param tp: float > 0, take profit price of stop-order. 2983 :param sl: float > 0, stop loss price of stop-order. 2984 :param expDate: it's a local date in future. 2985 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2986 :return: JSON with response from broker server. 2987 """ 2988 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2989 2990 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2991 """ 2992 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2993 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2994 2995 See also: `Order()` and `Trade()` docstrings. 2996 2997 :param lots: volume, integer count of lots >= 1. 2998 :param tp: float > 0, take profit price of stop-order. 2999 :param sl: float > 0, stop loss price of stop-order. 3000 :param expDate: it's a local date in the future. 3001 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3002 :return: JSON with response from broker server. 3003 """ 3004 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3005 3006 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3007 """ 3008 Close position of given instruments. 3009 3010 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3011 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3012 This avoids unnecessary downloading data from the server. 3013 """ 3014 if instruments is None or not instruments: 3015 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3016 raise Exception("Ticker or FIGI required") 3017 3018 if isinstance(instruments, str): 3019 instruments = [instruments] 3020 3021 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3022 if uniqueInstruments: 3023 if portfolio is None or not portfolio: 3024 portfolio = self.Overview(show=False) 3025 3026 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3027 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3028 3029 for self.figi in uniqueInstruments: 3030 if self.figi not in allOpened: 3031 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3032 continue 3033 3034 # search open trade info about instrument by ticker: 3035 instrument = {} 3036 for iType in TKS_INSTRUMENTS: 3037 if instrument: 3038 break 3039 3040 for item in portfolio["stat"][iType]: 3041 if item["figi"] == self.figi: 3042 instrument = item 3043 break 3044 3045 if instrument: 3046 self.ticker = instrument["ticker"] 3047 self.figi = instrument["figi"] 3048 3049 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3050 self.ticker, 3051 self.figi, 3052 int(instrument["volume"]), 3053 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3054 )) 3055 3056 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3057 3058 if tradeLots > 0: 3059 if instrument["blocked"] > 0: 3060 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3061 instrument["blocked"], 3062 self.ticker, 3063 tradeLots, 3064 )) 3065 3066 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3067 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3068 3069 else: 3070 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3071 3072 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3073 """ 3074 Close all positions of given instruments with defined type. 3075 3076 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3077 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3078 This avoids unnecessary downloading data from the server. 3079 """ 3080 if iType not in TKS_INSTRUMENTS: 3081 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3082 3083 else: 3084 if portfolio is None or not portfolio: 3085 portfolio = self.Overview(show=False) 3086 3087 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3088 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3089 3090 if tickers and portfolio: 3091 self.CloseTrades(tickers, portfolio) 3092 3093 else: 3094 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3095 3096 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3097 """ 3098 Universal method to create market or limit orders with all available parameters for current `accountId`. 3099 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3100 3101 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3102 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3103 3104 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3105 then broker immediately open market order as you can do simple --buy or --sell operations! 3106 3107 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3108 When current price will go up or down to target price value then broker opens a limit order. 3109 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3110 3111 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3112 3113 :param operation: string "Buy" or "Sell". 3114 :param orderType: string "Limit" or "Stop". 3115 :param lots: volume, integer count of lots >= 1. 3116 :param targetPrice: target price > 0. This is open trade price for limit order. 3117 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3118 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3119 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3120 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3121 Stop loss order always executed by market price. 3122 :param expDate: string "Undefined" by default or local date in future. 3123 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3124 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3125 A limit order has no expiration date, it lasts until the end of the trading day. 3126 :return: JSON with response from broker server. 3127 """ 3128 if self.accountId is None or not self.accountId: 3129 uLogger.error("Variable `accountId` must be defined for using this method!") 3130 raise Exception("Account ID required") 3131 3132 if operation is None or not operation or operation not in ("Buy", "Sell"): 3133 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3134 raise Exception("Incorrect value") 3135 3136 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3137 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3138 raise Exception("Incorrect value") 3139 3140 if lots is None or lots < 1: 3141 uLogger.error("You must define trade volume > 0: integer count of lots!") 3142 raise Exception("Incorrect value") 3143 3144 if targetPrice is None or targetPrice <= 0: 3145 uLogger.error("Target price for limit-order must be greater than 0!") 3146 raise Exception("Incorrect value") 3147 3148 if limitPrice is None or limitPrice <= 0: 3149 limitPrice = targetPrice 3150 3151 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3152 stopType = "Limit" 3153 3154 if expDate is None or not expDate: 3155 expDate = "Undefined" 3156 3157 if not (self.ticker or self.figi): 3158 uLogger.error("Tocker or FIGI must be defined!") 3159 raise Exception("Ticker or FIGI required") 3160 3161 response = {} 3162 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3163 self.ticker = instrument["ticker"] 3164 self.figi = instrument["figi"] 3165 3166 if orderType == "Limit": 3167 uLogger.debug( 3168 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3169 self.ticker, self.figi, 3170 operation, lots, targetPrice, instrument["currency"], 3171 )) 3172 3173 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3174 self.body = str({ 3175 "figi": self.figi, 3176 "quantity": str(lots), 3177 "price": FloatToNano(targetPrice), 3178 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3179 "accountId": str(self.accountId), 3180 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3181 }) 3182 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3183 3184 if "orderId" in response.keys(): 3185 uLogger.info( 3186 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3187 response["orderId"], 3188 self.ticker, self.figi, 3189 operation, lots, targetPrice, instrument["currency"], 3190 )) 3191 3192 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3193 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3194 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3195 targetPrice, instrument["currency"], 3196 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3197 )) 3198 3199 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3200 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3201 targetPrice, instrument["currency"], 3202 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3203 )) 3204 3205 else: 3206 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3207 3208 if orderType == "Stop": 3209 uLogger.debug( 3210 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3211 self.ticker, self.figi, 3212 operation, lots, 3213 targetPrice, instrument["currency"], 3214 limitPrice, instrument["currency"], 3215 stopType, expDate, 3216 )) 3217 3218 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3219 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3220 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3221 3222 body = { 3223 "figi": self.figi, 3224 "quantity": str(lots), 3225 "price": FloatToNano(limitPrice), 3226 "stopPrice": FloatToNano(targetPrice), 3227 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3228 "accountId": str(self.accountId), 3229 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3230 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3231 } 3232 3233 if expDateUTC: 3234 body["expireDate"] = expDateUTC 3235 3236 self.body = str(body) 3237 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3238 3239 if "stopOrderId" in response.keys(): 3240 uLogger.info( 3241 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3242 response["stopOrderId"], 3243 self.ticker, self.figi, 3244 operation, lots, 3245 targetPrice, instrument["currency"], 3246 limitPrice, instrument["currency"], 3247 TKS_STOP_ORDER_TYPES[stopOrderType], 3248 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3249 )) 3250 3251 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3252 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3253 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3254 targetPrice, instrument["currency"], 3255 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3256 )) 3257 3258 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3259 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3260 targetPrice, instrument["currency"], 3261 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3262 )) 3263 3264 else: 3265 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3266 3267 return response 3268 3269 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3270 """ 3271 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3272 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3273 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3274 See also: `Order()` docstring. 3275 3276 :param lots: volume, integer count of lots >= 1. 3277 :param targetPrice: target price > 0. This is open trade price for limit order. 3278 :return: JSON with response from broker server. 3279 """ 3280 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3281 3282 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3283 """ 3284 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3285 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3286 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3287 target price value then broker opens a limit order. See also: `Order()` docstring. 3288 3289 :param lots: volume, integer count of lots >= 1. 3290 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3291 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3292 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3293 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3294 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3295 :param expDate: string "Undefined" by default or local date in future. 3296 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3297 This date is converting to UTC format for server. 3298 :return: JSON with response from broker server. 3299 """ 3300 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3301 3302 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3303 """ 3304 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3305 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3306 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3307 See also: `Order()` docstring. 3308 3309 :param lots: volume, integer count of lots >= 1. 3310 :param targetPrice: target price > 0. This is open trade price for limit order. 3311 :return: JSON with response from broker server. 3312 """ 3313 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3314 3315 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3316 """ 3317 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3318 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3319 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3320 target price value then broker opens a limit order. See also: `Order()` docstring. 3321 3322 :param lots: volume, integer count of lots >= 1. 3323 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3324 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3325 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3326 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3327 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3328 :param expDate: string "Undefined" by default or local date in future. 3329 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3330 This date is converting to UTC format for server. 3331 :return: JSON with response from broker server. 3332 """ 3333 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3334 3335 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3336 """ 3337 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3338 3339 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3340 :param allOrdersIDs: pre-received lists of all active pending orders. 3341 This avoids unnecessary downloading data from the server. 3342 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3343 """ 3344 if self.accountId is None or not self.accountId: 3345 uLogger.error("Variable `accountId` must be defined for using this method!") 3346 raise Exception("Account ID required") 3347 3348 if orderIDs: 3349 if allOrdersIDs is None or not allOrdersIDs: 3350 rawOrders = self.RequestPendingOrders() 3351 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3352 3353 if allStopOrdersIDs is None or not allStopOrdersIDs: 3354 rawStopOrders = self.RequestStopOrders() 3355 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3356 3357 for orderID in orderIDs: 3358 idInPendingOrders = orderID in allOrdersIDs 3359 idInStopOrders = orderID in allStopOrdersIDs 3360 3361 if not (idInPendingOrders or idInStopOrders): 3362 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3363 continue 3364 3365 else: 3366 if idInPendingOrders: 3367 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3368 3369 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3370 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3371 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3372 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3373 3374 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3375 if self.moreDebug: 3376 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3377 3378 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3379 3380 else: 3381 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3382 3383 elif idInStopOrders: 3384 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3385 3386 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3387 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3388 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3389 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3390 3391 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3392 if self.moreDebug: 3393 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3394 3395 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3396 3397 else: 3398 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3399 3400 else: 3401 continue 3402 3403 def CloseAllOrders(self) -> None: 3404 """ 3405 Gets a list of open pending and stop orders and cancel it all. 3406 """ 3407 rawOrders = self.RequestPendingOrders() 3408 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3409 lenOrders = len(allOrdersIDs) 3410 3411 rawStopOrders = self.RequestStopOrders() 3412 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3413 lenSOrders = len(allStopOrdersIDs) 3414 3415 if lenOrders > 0 or lenSOrders > 0: 3416 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3417 3418 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3419 3420 else: 3421 uLogger.info("Orders not found, nothing to cancel.") 3422 3423 def CloseAll(self, *args) -> None: 3424 """ 3425 Close all available (not blocked) opened trades and orders. 3426 3427 Also, you can select one or more keywords case-insensitive: 3428 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3429 3430 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3431 """ 3432 overview = self.Overview(show=False) # get all open trades info 3433 3434 if len(args) == 0: 3435 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3436 self.CloseAllOrders() # close all pending and stop orders 3437 3438 for iType in TKS_INSTRUMENTS: 3439 if iType != "Currencies": 3440 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3441 3442 else: 3443 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3444 lowerArgs = [x.lower() for x in args] 3445 3446 if "orders" in lowerArgs: 3447 self.CloseAllOrders() # close all pending and stop orders 3448 3449 for iType in TKS_INSTRUMENTS: 3450 if iType.lower() in lowerArgs and iType != "Currencies": 3451 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3452 3453 @staticmethod 3454 def ParseOrderParameters(operation, **inputParameters): 3455 """ 3456 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3457 3458 :param operation: string "Buy" or "Sell". 3459 :param inputParameters: this is dict of strings that looks like this 3460 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3461 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3462 "prices" key: one or more prices to open limit-orders 3463 Counts of values in lots and prices lists must be equals! 3464 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3465 """ 3466 # TODO: update order grid work with api v2 3467 pass 3468 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3469 # 3470 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3471 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3472 # raise Exception("Incorrect value") 3473 # 3474 # if "l" in inputParameters.keys(): 3475 # inputParameters["lots"] = inputParameters.pop("l") 3476 # 3477 # if "p" in inputParameters.keys(): 3478 # inputParameters["prices"] = inputParameters.pop("p") 3479 # 3480 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3481 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3482 # raise Exception("Incorrect value") 3483 # 3484 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3485 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3486 # 3487 # if len(lots) != len(prices): 3488 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3489 # raise Exception("Incorrect value") 3490 # 3491 # uLogger.debug("Extracted parameters for orders:") 3492 # uLogger.debug("lots = {}".format(lots)) 3493 # uLogger.debug("prices = {}".format(prices)) 3494 # 3495 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3496 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3497 # uLogger.debug("Order parameters: {}".format(result)) 3498 # 3499 # return result 3500 3501 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3502 """ 3503 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3504 3505 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3506 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3507 """ 3508 result = False 3509 msg = "Instrument not defined!" 3510 3511 if portfolio is None or not portfolio: 3512 portfolio = self.Overview(show=False) 3513 3514 if self.ticker: 3515 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3516 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3517 3518 for iType in TKS_INSTRUMENTS: 3519 for instrument in portfolio["stat"][iType]: 3520 if instrument["ticker"] == self.ticker: 3521 result = True 3522 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3523 break 3524 3525 elif self.figi: 3526 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3527 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3528 3529 for iType in TKS_INSTRUMENTS: 3530 for instrument in portfolio["stat"][iType]: 3531 if instrument["figi"] == self.figi: 3532 result = True 3533 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3534 break 3535 3536 else: 3537 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3538 3539 uLogger.debug(msg) 3540 3541 return result 3542 3543 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3544 """ 3545 Returns instrument is in the user's portfolio if it presents there. 3546 Instrument must be defined by `ticker` (highly priority) or `figi`. 3547 3548 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3549 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3550 """ 3551 result = None 3552 msg = "Instrument not defined!" 3553 3554 if portfolio is None or not portfolio: 3555 portfolio = self.Overview(show=False) 3556 3557 if self.ticker: 3558 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3559 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3560 3561 for iType in TKS_INSTRUMENTS: 3562 for instrument in portfolio["stat"][iType]: 3563 if instrument["ticker"] == self.ticker: 3564 result = instrument 3565 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3566 break 3567 3568 elif self.figi: 3569 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3570 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3571 3572 for iType in TKS_INSTRUMENTS: 3573 for instrument in portfolio["stat"][iType]: 3574 if instrument["figi"] == self.figi: 3575 result = instrument 3576 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3577 break 3578 3579 else: 3580 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3581 3582 uLogger.debug(msg) 3583 3584 return result 3585 3586 def RequestLimits(self) -> dict: 3587 """ 3588 Method for obtaining the available funds for withdrawal for current `accountId`. 3589 3590 See also: 3591 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3592 - `OverviewLimits()` method 3593 3594 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3595 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3596 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3597 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3598 """ 3599 if self.accountId is None or not self.accountId: 3600 uLogger.error("Variable `accountId` must be defined for using this method!") 3601 raise Exception("Account ID required") 3602 3603 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3604 3605 self.body = str({"accountId": self.accountId}) 3606 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3607 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3608 3609 if self.moreDebug: 3610 uLogger.debug("Records about available funds for withdrawal successfully received") 3611 3612 return rawLimits 3613 3614 def OverviewLimits(self, show: bool = False) -> dict: 3615 """ 3616 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3617 3618 See also: `RequestLimits()`. 3619 3620 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3621 :return: dict with raw parsed data from server and some calculated statistics about it. 3622 """ 3623 if self.accountId is None or not self.accountId: 3624 uLogger.error("Variable `accountId` must be defined for using this method!") 3625 raise Exception("Account ID required") 3626 3627 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3628 3629 view = { 3630 "rawLimits": rawLimits, 3631 "limits": { # parsed data for every currency: 3632 "money": { # this is an array of portfolio currency positions 3633 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3634 }, 3635 "blocked": { # this is an array of blocked currency 3636 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3637 }, 3638 "blockedGuarantee": { # this is locked money under collateral for futures 3639 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3640 }, 3641 }, 3642 } 3643 3644 # --- Prepare text table with limits in human-readable format: 3645 if show: 3646 info = [ 3647 "# Withdrawal limits\n\n", 3648 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3649 "* **Account ID:** [{}]\n".format(self.accountId), 3650 ] 3651 3652 if view["limits"]["money"]: 3653 info.extend([ 3654 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3655 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3656 ]) 3657 3658 else: 3659 info.append("\nNo withdrawal limits\n") 3660 3661 for curr in view["limits"]["money"].keys(): 3662 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3663 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3664 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3665 3666 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3667 "[{}]".format(curr), 3668 "{:.2f}".format(view["limits"]["money"][curr]), 3669 "{:.2f}".format(availableMoney), 3670 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3671 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3672 ) 3673 3674 if curr == "rub": 3675 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3676 3677 else: 3678 info.append(infoStr) 3679 3680 infoText = "".join(info) 3681 3682 uLogger.info(infoText) 3683 3684 if self.withdrawalLimitsFile: 3685 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3686 fH.write(infoText) 3687 3688 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3689 3690 return view 3691 3692 def RequestAccounts(self) -> dict: 3693 """ 3694 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3695 3696 See also: 3697 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3698 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3699 - `OverviewUserInfo()` method 3700 3701 :return: dict with raw data from server that contains accounts info. Example of dict: 3702 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3703 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3704 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3705 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3706 """ 3707 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3708 3709 self.body = str({}) 3710 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3711 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3712 3713 if self.moreDebug: 3714 uLogger.debug("Records about available accounts successfully received") 3715 3716 return rawAccounts 3717 3718 def RequestUserInfo(self) -> dict: 3719 """ 3720 Method for requesting common user's information. 3721 3722 See also: 3723 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3724 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3725 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3726 - `OverviewUserInfo()` method 3727 3728 :return: dict with raw data from server that contains user's information. Example of dict: 3729 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3730 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3731 """ 3732 uLogger.debug("Requesting common user's information. Wait, please...") 3733 3734 self.body = str({}) 3735 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3736 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3737 3738 if self.moreDebug: 3739 uLogger.debug("Records about current user successfully received") 3740 3741 return rawUserInfo 3742 3743 def RequestMarginStatus(self, accountId: str = None) -> dict: 3744 """ 3745 Method for requesting margin calculation for defined account ID. 3746 3747 See also: 3748 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3749 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3750 - `OverviewUserInfo()` method 3751 3752 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3753 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3754 Example of responses: 3755 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3756 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3757 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3758 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3759 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3760 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3761 """ 3762 if accountId is None or not accountId: 3763 if self.accountId is None or not self.accountId: 3764 uLogger.error("Variable `accountId` must be defined for using this method!") 3765 raise Exception("Account ID required") 3766 3767 else: 3768 accountId = self.accountId # use `self.accountId` (main ID) by default 3769 3770 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3771 3772 self.body = str({"accountId": accountId}) 3773 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3774 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3775 3776 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3777 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3778 rawMargin = {} 3779 3780 else: 3781 if self.moreDebug: 3782 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3783 3784 return rawMargin 3785 3786 def RequestTariffLimits(self) -> dict: 3787 """ 3788 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3789 3790 See also: 3791 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3792 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3793 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3794 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3795 - `OverviewUserInfo()` method 3796 3797 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3798 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3799 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3800 """ 3801 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3802 3803 self.body = str({}) 3804 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3805 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3806 3807 if self.moreDebug: 3808 uLogger.debug("Records with limits of current tariff successfully received") 3809 3810 return rawTariffLimits 3811 3812 def RequestBondCoupons(self, iJSON: dict) -> dict: 3813 """ 3814 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3815 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3816 All dates are in UTC timezone. 3817 3818 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3819 Documentation: 3820 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3821 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3822 3823 See also: `ExtendBondsData()`. 3824 3825 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3826 If raw iJSON is not data of bond then server returns an error [400] with message: 3827 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3828 :return: dictionary with bond payment calendar. Response example 3829 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3830 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3831 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3832 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3833 """ 3834 if iJSON["figi"] is None or not iJSON["figi"]: 3835 uLogger.error("FIGI must be defined for using this method!") 3836 raise Exception("FIGI required") 3837 3838 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3839 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3840 3841 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3842 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3843 self.figi, 3844 startDate, 3845 endDate, 3846 )) 3847 3848 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3849 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3850 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3851 3852 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3853 uLogger.warning("Instrument type is not bond!") 3854 3855 else: 3856 if self.moreDebug: 3857 uLogger.debug("Records about bond payment calendar successfully received") 3858 3859 return calendar 3860 3861 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3862 """ 3863 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3864 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3865 coupon yields, current yields and some statistics etc. 3866 3867 WARNING! This is too long operation if a lot of bonds requested from broker server. 3868 3869 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3870 3871 :param instruments: list of strings with tickers or FIGIs. 3872 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3873 for further used by data scientists or stock analytics. 3874 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3875 In XLSX-file and Pandas DataFrame fields mean: 3876 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3877 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3878 """ 3879 if instruments is None or not instruments: 3880 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3881 raise Exception("Ticker or FIGI required") 3882 3883 if isinstance(instruments, str): 3884 instruments = [instruments] 3885 3886 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3887 3888 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3889 3890 iCount = len(uniqueInstruments) 3891 tooLong = iCount >= 20 3892 if tooLong: 3893 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3894 3895 bonds = None 3896 for i, self.figi in enumerate(uniqueInstruments): 3897 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3898 3899 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3900 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3901 rawBond = self.SearchByFIGI(requestPrice=True) 3902 3903 # Widen raw data with UTC current time (iData["actualDateTime"]): 3904 actualDate = datetime.now(tzutc()) 3905 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3906 3907 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3908 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3909 3910 # Replace some values with human-readable: 3911 iData["nominalCurrency"] = iData["nominal"]["currency"] 3912 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3913 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3914 iData["aciCurrency"] = iData["aciValue"]["currency"] 3915 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3916 iData["issueSize"] = int(iData["issueSize"]) 3917 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3918 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3919 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3920 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3921 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3922 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3923 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3924 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3925 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3926 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3927 3928 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3929 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3930 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3931 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3932 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3933 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3934 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3935 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3936 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3937 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3938 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3939 3940 # Widen raw data with calendar data from `rawCalendar` values: 3941 calendarData = [] 3942 if "events" in iData["rawCalendar"].keys(): 3943 for item in iData["rawCalendar"]["events"]: 3944 calendarData.append({ 3945 "couponDate": item["couponDate"], 3946 "couponNumber": int(item["couponNumber"]), 3947 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3948 "payCurrency": item["payOneBond"]["currency"], 3949 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3950 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3951 "couponStartDate": item["couponStartDate"], 3952 "couponEndDate": item["couponEndDate"], 3953 "couponPeriod": item["couponPeriod"], 3954 }) 3955 3956 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3957 if "maturityDate" not in iData.keys(): 3958 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3959 3960 # Widen raw data with Coupon Rate. 3961 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3962 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3963 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3964 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3965 3966 # Widen raw data with Yield to Maturity (YTM) on current date. 3967 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3968 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3969 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3970 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3971 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3972 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3973 3974 iData["calendar"] = calendarData # adds calendar at the end 3975 3976 # Remove not used data: 3977 iData.pop("uid") 3978 iData.pop("positionUid") 3979 iData.pop("currentPrice") 3980 iData.pop("rawCalendar") 3981 3982 colNames = list(iData.keys()) 3983 if bonds is None: 3984 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3985 3986 else: 3987 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3988 3989 else: 3990 uLogger.warning("Instrument is not a bond!") 3991 3992 processed = round(100 * (i + 1) / iCount, 1) 3993 if tooLong and processed % 5 == 0: 3994 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3995 3996 else: 3997 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3998 3999 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4000 4001 # Saving bonds from Pandas DataFrame to XLSX sheet: 4002 if xlsx and self.bondsXLSXFile: 4003 with pd.ExcelWriter( 4004 path=self.bondsXLSXFile, 4005 date_format=TKS_DATE_FORMAT, 4006 datetime_format=TKS_DATE_TIME_FORMAT, 4007 mode="w", 4008 ) as writer: 4009 bonds.to_excel( 4010 writer, 4011 sheet_name="Extended bonds data", 4012 index=True, 4013 encoding="UTF-8", 4014 freeze_panes=(1, 1), 4015 ) # saving as XLSX-file with freeze first row and column as headers 4016 4017 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4018 4019 return bonds 4020 4021 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4022 """ 4023 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4024 4025 WARNING! This is too long operation if a lot of bonds requested from broker server. 4026 4027 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4028 4029 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4030 extended information about bonds: main info, current prices, bond payment calendar, 4031 coupon yields, current yields and some statistics etc. 4032 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4033 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4034 for further used by data scientists or stock analytics. 4035 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4036 """ 4037 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4038 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4039 4040 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4041 4042 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4043 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4044 calendar = None 4045 for bond in extBonds.iterrows(): 4046 for item in bond[1]["calendar"]: 4047 cData = { 4048 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4049 "couponDate": item["couponDate"], 4050 "figi": bond[1]["figi"], 4051 "ticker": bond[1]["ticker"], 4052 "name": bond[1]["name"], 4053 "couponNumber": item["couponNumber"], 4054 "payOneBond": item["payOneBond"], 4055 "payCurrency": item["payCurrency"], 4056 "couponType": item["couponType"], 4057 "couponPeriod": item["couponPeriod"], 4058 "fixDate": item["fixDate"], 4059 "couponStartDate": item["couponStartDate"], 4060 "couponEndDate": item["couponEndDate"], 4061 } 4062 4063 if calendar is None: 4064 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4065 4066 else: 4067 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4068 4069 if calendar is not None: 4070 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4071 4072 # Saving calendar from Pandas DataFrame to XLSX sheet: 4073 if xlsx: 4074 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4075 4076 with pd.ExcelWriter( 4077 path=xlsxCalendarFile, 4078 date_format=TKS_DATE_FORMAT, 4079 datetime_format=TKS_DATE_TIME_FORMAT, 4080 mode="w", 4081 ) as writer: 4082 humanReadable = calendar.copy(deep=True) 4083 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4084 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4085 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4086 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4087 humanReadable.columns = colNames # human-readable column names 4088 4089 humanReadable.to_excel( 4090 writer, 4091 sheet_name="Bond payments calendar", 4092 index=False, 4093 encoding="UTF-8", 4094 freeze_panes=(1, 2), 4095 ) # saving as XLSX-file with freeze first row and column as headers 4096 4097 del humanReadable # release df in memory 4098 4099 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4100 4101 return calendar 4102 4103 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4104 """ 4105 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4106 Also, creates Markdown file with calendar data, `calendar.md` by default. 4107 4108 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4109 4110 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4111 extended information about bonds: main info, current prices, bond payment calendar, 4112 coupon yields, current yields and some statistics etc. 4113 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4114 :param show: if `True` then also printing bonds payment calendar to the console, 4115 otherwise save to file `calendarFile` only. `False` by default. 4116 :return: multilines text in Markdown format with bonds payment calendar as a table. 4117 """ 4118 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4119 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4120 4121 infoText = "# Bond payments calendar\n\n" 4122 4123 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4124 4125 if not (calendar is None or calendar.empty): 4126 splitLine = "| | | | | | | | | |\n" 4127 4128 info = [ 4129 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4130 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4131 ] 4132 4133 newMonth = False 4134 notOneBond = calendar["figi"].nunique() > 1 4135 for i, bond in enumerate(calendar.iterrows()): 4136 if newMonth and notOneBond: 4137 info.append(splitLine) 4138 4139 info.append( 4140 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4141 " √" if bond[1]["paid"] else " —", 4142 bond[1]["couponDate"].split("T")[0], 4143 bond[1]["figi"], 4144 bond[1]["ticker"], 4145 bond[1]["couponNumber"], 4146 "{} {}".format( 4147 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4148 bond[1]["payCurrency"], 4149 ), 4150 bond[1]["couponType"], 4151 bond[1]["couponPeriod"], 4152 bond[1]["fixDate"].split("T")[0], 4153 ) 4154 ) 4155 4156 if i < len(calendar.values) - 1: 4157 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4158 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4159 newMonth = False if curDate.month == nextDate.month else True 4160 4161 else: 4162 newMonth = False 4163 4164 infoText += "".join(info) 4165 4166 if show: 4167 uLogger.info("{}".format(infoText)) 4168 4169 if self.calendarFile is not None: 4170 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4171 fH.write(infoText) 4172 4173 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4174 4175 else: 4176 infoText += "No data\n" 4177 4178 return infoText 4179 4180 def OverviewAccounts(self, show: bool = False) -> dict: 4181 """ 4182 Method for parsing and show simple table with all available user accounts. 4183 4184 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4185 4186 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4187 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4188 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4189 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4190 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4191 "closed": "—", "access": "Full access" }, ...}}` 4192 """ 4193 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4194 4195 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4196 accounts = { 4197 item["id"]: { 4198 "type": TKS_ACCOUNT_TYPES[item["type"]], 4199 "name": item["name"], 4200 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4201 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4202 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4203 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4204 } for item in rawAccounts["accounts"] 4205 } 4206 4207 # Raw and parsed data with some fields replaced in "stat" section: 4208 view = { 4209 "rawAccounts": rawAccounts, 4210 "stat": accounts, 4211 } 4212 4213 # --- Prepare simple text table with only accounts data in human-readable format: 4214 if show: 4215 info = [ 4216 "# User accounts\n\n", 4217 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4218 "| Account ID | Type | Status | Name |\n", 4219 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4220 ] 4221 4222 for account in view["stat"].keys(): 4223 info.extend([ 4224 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4225 account, 4226 view["stat"][account]["type"], 4227 view["stat"][account]["status"], 4228 view["stat"][account]["name"], 4229 ) 4230 ]) 4231 4232 infoText = "".join(info) 4233 4234 uLogger.info(infoText) 4235 4236 if self.userAccountsFile: 4237 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4238 fH.write(infoText) 4239 4240 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4241 4242 return view 4243 4244 def OverviewUserInfo(self, show: bool = False) -> dict: 4245 """ 4246 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4247 4248 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4249 4250 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4251 :return: dict with raw parsed data from server and some calculated statistics about it. 4252 """ 4253 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4254 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4255 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4256 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4257 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4258 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4259 4260 # This is dict with parsed common user data: 4261 userInfo = { 4262 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4263 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4264 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4265 "tariff": rawUserInfo["tariff"], 4266 } 4267 4268 # This is an array of dict with parsed margin statuses for every account IDs: 4269 margins = {} 4270 for accountId in accounts.keys(): 4271 if rawMargins[accountId]: 4272 margins[accountId] = { 4273 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4274 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4275 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4276 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4277 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4278 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4279 } 4280 4281 else: 4282 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4283 4284 unary = {} # unary-connection limits 4285 for item in rawTariffLimits["unaryLimits"]: 4286 if item["limitPerMinute"] in unary.keys(): 4287 unary[item["limitPerMinute"]].extend(item["methods"]) 4288 4289 else: 4290 unary[item["limitPerMinute"]] = item["methods"] 4291 4292 stream = {} # stream-connection limits 4293 for item in rawTariffLimits["streamLimits"]: 4294 if item["limit"] in stream.keys(): 4295 stream[item["limit"]].extend(item["streams"]) 4296 4297 else: 4298 stream[item["limit"]] = item["streams"] 4299 4300 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4301 limits = { 4302 "unary": unary, 4303 "stream": stream, 4304 } 4305 4306 # Raw and parsed data as an output result: 4307 view = { 4308 "rawUserInfo": rawUserInfo, 4309 "rawAccounts": rawAccounts, 4310 "rawMargins": rawMargins, 4311 "rawTariffLimits": rawTariffLimits, 4312 "stat": { 4313 "userInfo": userInfo, 4314 "accounts": accounts, 4315 "margins": margins, 4316 "limits": limits, 4317 }, 4318 } 4319 4320 # --- Prepare text table with user information in human-readable format: 4321 if show: 4322 info = [ 4323 "# Full user information\n\n", 4324 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4325 "## Common information\n\n", 4326 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4327 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4328 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4329 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4330 "\n## User accounts\n\n", 4331 ] 4332 4333 for account in view["stat"]["accounts"].keys(): 4334 info.extend([ 4335 "### ID: [{}]\n\n".format(account), 4336 "| Parameters | Values |\n", 4337 "|----------------------|--------------------------------------------------------------|\n", 4338 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4339 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4340 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4341 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4342 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4343 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4344 ]) 4345 4346 if margins[account]: 4347 info.extend([ 4348 "| Margin status: | Enabled |\n", 4349 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4350 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4351 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4352 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4353 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4354 ]) 4355 4356 else: 4357 info.append("| Margin status: | Disabled |\n\n") 4358 4359 info.extend([ 4360 "\n## Current user tariff limits\n", 4361 "\nSee also:\n", 4362 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4363 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4364 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4365 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4366 "\n### Unary limits\n", 4367 ]) 4368 4369 if unary: 4370 for key, values in sorted(unary.items()): 4371 info.append("\n* Max requests per minute: {}\n".format(key)) 4372 4373 for value in values: 4374 info.append(" - {}\n".format(value)) 4375 4376 else: 4377 info.append("\nNot available\n") 4378 4379 info.append("\n### Stream limits\n") 4380 4381 if stream: 4382 for key, values in sorted(stream.items()): 4383 info.append("\n* Max stream connections: {}\n".format(key)) 4384 4385 for value in values: 4386 info.append(" - {}\n".format(value)) 4387 4388 else: 4389 info.append("\nNot available\n") 4390 4391 infoText = "".join(info) 4392 4393 uLogger.info(infoText) 4394 4395 if self.userInfoFile: 4396 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4397 fH.write(infoText) 4398 4399 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4400 4401 return view 4402 4403 4404class Args: 4405 """ 4406 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4407 """ 4408 def __init__(self, **kwargs): 4409 self.__dict__.update(kwargs) 4410 4411 def __getattr__(self, item): 4412 return None 4413 4414 4415def ParseArgs(): 4416 """This function get and parse command line keys.""" 4417 parser = ArgumentParser() # command-line string parser 4418 4419 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4420 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4421 4422 # --- options: 4423 4424 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4425 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4426 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4427 4428 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4429 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4430 4431 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4432 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4433 4434 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4435 4436 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4437 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4438 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4439 4440 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4441 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4442 4443 # --- commands: 4444 4445 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4446 4447 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4448 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4449 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4450 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4451 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4452 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4453 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4454 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4455 4456 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4457 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4458 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4459 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4460 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4461 4462 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4463 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4464 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4465 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4466 4467 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4468 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4469 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4470 4471 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4472 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4473 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4474 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4475 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4476 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4477 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4478 4479 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4480 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4481 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4482 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4483 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4484 4485 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4486 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4487 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4488 4489 cmdArgs = parser.parse_args() 4490 return cmdArgs 4491 4492 4493def Main(**kwargs): 4494 """ 4495 Main function for work with TKSBrokerAPI in the console. 4496 4497 See examples: 4498 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4499 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4500 """ 4501 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4502 4503 if args.debug_level: 4504 uLogger.level = 10 # always debug level by default 4505 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4506 4507 exitCode = 0 4508 start = datetime.now(tzutc()) 4509 uLogger.debug("=-" * 60) 4510 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4511 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4512 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4513 )) 4514 4515 # trying to calculate full current version: 4516 buildVersion = __version__ 4517 try: 4518 v = version("tksbrokerapi") 4519 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4520 4521 except Exception: 4522 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4523 4524 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4525 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4526 4527 try: 4528 if args.version: 4529 print("TKSBrokerAPI {}".format(buildVersion)) 4530 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4531 4532 else: 4533 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4534 server = TinkoffBrokerServer( 4535 token=args.token, 4536 accountId=args.account_id, 4537 useCache=not args.no_cache, 4538 ) 4539 4540 # --- set some options: 4541 4542 if args.more: 4543 server.moreDebug = True 4544 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4545 4546 if args.ticker: 4547 if args.ticker in server.aliasesKeys: 4548 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4549 4550 else: 4551 server.ticker = args.ticker 4552 4553 if args.figi: 4554 server.figi = args.figi 4555 4556 if args.depth is not None: 4557 server.depth = args.depth 4558 4559 # --- do one command: 4560 4561 if args.list: 4562 if args.output is not None: 4563 server.instrumentsFile = args.output 4564 4565 server.ShowInstrumentsInfo(show=True) 4566 4567 elif args.list_xlsx: 4568 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4569 4570 elif args.bonds_xlsx is not None: 4571 if args.output is not None: 4572 server.bondsXLSXFile = args.output 4573 4574 if len(args.bonds_xlsx) == 0: 4575 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4576 4577 else: 4578 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4579 4580 elif args.search: 4581 if args.output is not None: 4582 server.searchResultsFile = args.output 4583 4584 server.SearchInstruments(pattern=args.search[0], show=True) 4585 4586 elif args.info: 4587 if not (args.ticker or args.figi): 4588 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4589 raise Exception("Ticker or FIGI required") 4590 4591 if args.output is not None: 4592 server.infoFile = args.output 4593 4594 if args.ticker: 4595 server.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4596 4597 else: 4598 server.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4599 4600 elif args.calendar is not None: 4601 if args.output is not None: 4602 server.calendarFile = args.output 4603 4604 if len(args.calendar) == 0: 4605 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4606 4607 else: 4608 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4609 4610 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4611 4612 elif args.price: 4613 if not (args.ticker or args.figi): 4614 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4615 raise Exception("Ticker or FIGI required") 4616 4617 server.GetCurrentPrices(show=True) 4618 4619 elif args.prices is not None: 4620 if args.output is not None: 4621 server.pricesFile = args.output 4622 4623 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4624 4625 elif args.overview: 4626 if args.output is not None: 4627 server.overviewFile = args.output 4628 4629 server.Overview(show=True, details="full") 4630 4631 elif args.overview_digest: 4632 if args.output is not None: 4633 server.overviewDigestFile = args.output 4634 4635 server.Overview(show=True, details="digest") 4636 4637 elif args.overview_positions: 4638 if args.output is not None: 4639 server.overviewPositionsFile = args.output 4640 4641 server.Overview(show=True, details="positions") 4642 4643 elif args.overview_orders: 4644 if args.output is not None: 4645 server.overviewOrdersFile = args.output 4646 4647 server.Overview(show=True, details="orders") 4648 4649 elif args.overview_analytics: 4650 if args.output is not None: 4651 server.overviewAnalyticsFile = args.output 4652 4653 server.Overview(show=True, details="analytics") 4654 4655 elif args.deals is not None: 4656 if args.output is not None: 4657 server.reportFile = args.output 4658 4659 if 0 <= len(args.deals) < 3: 4660 server.Deals( 4661 start=args.deals[0] if len(args.deals) >= 1 else None, 4662 end=args.deals[1] if len(args.deals) == 2 else None, 4663 show=True, # Always show deals report in console 4664 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4665 ) 4666 4667 else: 4668 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4669 raise Exception("Incorrect value") 4670 4671 elif args.history is not None: 4672 if args.output is not None: 4673 server.historyFile = args.output 4674 4675 if 0 <= len(args.history) < 3: 4676 dataReceived = server.History( 4677 start=args.history[0] if len(args.history) >= 1 else None, 4678 end=args.history[1] if len(args.history) == 2 else None, 4679 interval="hour" if args.interval is None or not args.interval else args.interval, 4680 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4681 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4682 show=True, # shows all downloaded candles in console 4683 ) 4684 4685 if args.render_chart is not None and dataReceived is not None: 4686 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4687 4688 server.ShowHistoryChart( 4689 candles=dataReceived, 4690 interact=iChart, 4691 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4692 ) 4693 4694 else: 4695 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4696 raise Exception("Incorrect value") 4697 4698 elif args.load_history is not None: 4699 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4700 4701 if args.render_chart is not None and histData is not None: 4702 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4703 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4704 4705 server.ShowHistoryChart( 4706 candles=histData, 4707 interact=iChart, 4708 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4709 ) 4710 4711 elif args.trade is not None: 4712 if 1 <= len(args.trade) <= 5: 4713 server.Trade( 4714 operation=args.trade[0], 4715 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4716 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4717 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4718 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4719 ) 4720 4721 else: 4722 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4723 4724 elif args.buy is not None: 4725 if 0 <= len(args.buy) <= 4: 4726 server.Buy( 4727 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4728 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4729 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4730 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4731 ) 4732 4733 else: 4734 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4735 4736 elif args.sell is not None: 4737 if 0 <= len(args.sell) <= 4: 4738 server.Sell( 4739 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4740 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4741 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4742 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4743 ) 4744 4745 else: 4746 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4747 4748 elif args.order: 4749 if 4 <= len(args.order) <= 7: 4750 server.Order( 4751 operation=args.order[0], 4752 orderType=args.order[1], 4753 lots=int(args.order[2]), 4754 targetPrice=float(args.order[3]), 4755 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4756 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4757 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4758 ) 4759 4760 else: 4761 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4762 4763 elif args.buy_limit: 4764 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4765 4766 elif args.sell_limit: 4767 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4768 4769 elif args.buy_stop: 4770 if 2 <= len(args.buy_stop) <= 7: 4771 server.BuyStop( 4772 lots=int(args.buy_stop[0]), 4773 targetPrice=float(args.buy_stop[1]), 4774 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4775 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4776 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4777 ) 4778 4779 else: 4780 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4781 4782 elif args.sell_stop: 4783 if 2 <= len(args.sell_stop) <= 7: 4784 server.SellStop( 4785 lots=int(args.sell_stop[0]), 4786 targetPrice=float(args.sell_stop[1]), 4787 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4788 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4789 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4790 ) 4791 4792 else: 4793 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4794 4795 # elif args.buy_order_grid is not None: 4796 # # update order grid work with api v2 4797 # if len(args.buy_order_grid) == 2: 4798 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4799 # 4800 # for order in orderParams: 4801 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4802 # 4803 # else: 4804 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4805 # 4806 # elif args.sell_order_grid is not None: 4807 # # update order grid work with api v2 4808 # if len(args.sell_order_grid) >= 2: 4809 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4810 # 4811 # for order in orderParams: 4812 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4813 # 4814 # else: 4815 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4816 4817 elif args.close_order is not None: 4818 server.CloseOrders(args.close_order) # close only one order 4819 4820 elif args.close_orders is not None: 4821 server.CloseOrders(args.close_orders) # close list of orders 4822 4823 elif args.close_trade: 4824 if not (args.ticker or args.figi): 4825 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4826 raise Exception("Ticker or FIGI required") 4827 4828 if args.ticker: 4829 server.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4830 4831 else: 4832 server.CloseTrades([args.figi]) # close only one trade by FIGI 4833 4834 elif args.close_trades is not None: 4835 server.CloseTrades(args.close_trades) # close trades for list of tickers 4836 4837 elif args.close_all is not None: 4838 server.CloseAll(*args.close_all) 4839 4840 elif args.limits: 4841 if args.output is not None: 4842 server.withdrawalLimitsFile = args.output 4843 4844 server.OverviewLimits(show=True) 4845 4846 elif args.user_info: 4847 if args.output is not None: 4848 server.userInfoFile = args.output 4849 4850 server.OverviewUserInfo(show=True) 4851 4852 elif args.account: 4853 if args.output is not None: 4854 server.userAccountsFile = args.output 4855 4856 server.OverviewAccounts(show=True) 4857 4858 else: 4859 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4860 raise Exception("There is no command to execute") 4861 4862 except Exception: 4863 trace = tb.format_exc() 4864 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4865 if e in trace: 4866 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4867 break 4868 4869 uLogger.debug(trace) 4870 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4871 exitCode = 255 # an error occurred, must be open a ticket for this issue 4872 4873 finally: 4874 finish = datetime.now(tzutc()) 4875 4876 if exitCode == 0: 4877 if args.more: 4878 uLogger.debug("All operations were finished success (summary code is 0).") 4879 4880 else: 4881 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4882 os.path.abspath(uLog.defaultLogFile), exitCode, 4883 )) 4884 4885 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4886 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4887 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4888 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4889 )) 4890 uLogger.debug("=-" * 60) 4891 4892 if not kwargs: 4893 sys.exit(exitCode) 4894 4895 else: 4896 return exitCode 4897 4898 4899if __name__ == "__main__": 4900 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.moreDebug = False 301 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 302 303 self.historyFile = None 304 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 305 306 See also: `History()`. 307 """ 308 309 self.htmlHistoryFile = "index.html" 310 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 311 312 See also: `ShowHistoryChart()`. 313 """ 314 315 self.instrumentsFile = "instruments.md" 316 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 317 318 See also: `ShowInstrumentsInfo()`. 319 """ 320 321 self.searchResultsFile = "search-results.md" 322 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 323 324 See also: `SearchInstruments()`. 325 """ 326 327 self.pricesFile = "prices.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `GetListOfPrices()`. 331 """ 332 333 self.infoFile = "info.md" 334 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 335 336 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 337 """ 338 339 self.bondsXLSXFile = "ext-bonds.xlsx" 340 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 341 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 342 343 See also: `ExtendBondsData()`. 344 """ 345 346 self.calendarFile = "calendar.md" 347 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 348 349 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 350 351 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 352 """ 353 354 self.overviewFile = "overview.md" 355 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 356 357 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 358 """ 359 360 self.overviewDigestFile = "overview-digest.md" 361 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 362 363 See also: `Overview()` with parameter `details="digest"`. 364 """ 365 366 self.overviewPositionsFile = "overview-positions.md" 367 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 368 369 See also: `Overview()` with parameter `details="positions"`. 370 """ 371 372 self.overviewOrdersFile = "overview-orders.md" 373 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 374 375 See also: `Overview()` with parameter `details="orders"`. 376 """ 377 378 self.overviewAnalyticsFile = "overview-analytics.md" 379 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 380 381 See also: `Overview()` with parameter `details="analytics"`. 382 """ 383 384 self.reportFile = "deals.md" 385 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 386 387 See also: `Deals()`. 388 """ 389 390 self.withdrawalLimitsFile = "limits.md" 391 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 392 393 See also: `OverviewLimits()` and `RequestLimits()`. 394 """ 395 396 self.userInfoFile = "user-info.md" 397 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 398 399 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 400 """ 401 402 self.userAccountsFile = "accounts.md" 403 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 404 405 See also: `OverviewAccounts()`, `RequestAccounts()`. 406 """ 407 408 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 409 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 410 411 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 412 413 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 414 """ 415 416 self.iList = None # init iList for raw instruments data 417 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 418 419 See also: `Listing()`, `DumpInstruments()`. 420 """ 421 422 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 423 if useCache: 424 if os.path.exists(self.iListDumpFile): 425 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 426 curTime = datetime.now(tzutc()) 427 428 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 429 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 430 431 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 432 433 else: 434 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 435 436 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 437 os.path.abspath(self.iListDumpFile), 438 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 439 )) 440 441 else: 442 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 443 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 444 445 else: 446 self.iList = self.Listing() # request new raw instruments data from broker server 447 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 448 449 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 450 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 451 452 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 453 """ 454 455 def _ParseJSON(self, rawData="{}") -> dict: 456 """ 457 Parse JSON from response string. 458 459 :param rawData: this is a string with JSON-formatted text. 460 :return: JSON (dictionary), parsed from server response string. 461 """ 462 responseJSON = json.loads(rawData) if rawData else {} 463 464 if self.moreDebug: 465 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 466 467 return responseJSON 468 469 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 470 """ 471 Send GET or POST request to broker server and receive JSON object. 472 473 self.header: must be defining with dictionary of headers. 474 self.body: if define then used as request body. None by default. 475 self.timeout: global request timeout, 15 seconds by default. 476 :param url: url with REST request. 477 :param reqType: send "GET" or "POST" request. "GET" by default. 478 :param retry: how many times retry after first request if an 5xx server errors occurred. 479 :param pause: sleep time in seconds between retries. 480 :return: response JSON (dictionary) from broker. 481 """ 482 if reqType not in ("GET", "POST"): 483 uLogger.error("You can define request type: 'GET' or 'POST'!") 484 raise Exception("Incorrect value") 485 486 if self.moreDebug: 487 uLogger.debug("Request parameters:") 488 uLogger.debug(" - REST API URL: {}".format(url)) 489 uLogger.debug(" - request type: {}".format(reqType)) 490 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 491 uLogger.debug(" - body:\n{}".format(self.body)) 492 493 # fast hack to avoid all operations with some tickers/FIGI 494 responseJSON = {} 495 oK = True 496 for item in self.exclude: 497 if item in url: 498 if self.moreDebug: 499 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 500 501 oK = False 502 break 503 504 if oK: 505 counter = 0 506 response = None 507 errMsg = "" 508 509 while not response and counter <= retry: 510 if reqType == "GET": 511 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 512 513 if reqType == "POST": 514 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if self.moreDebug: 517 uLogger.debug("Response:") 518 uLogger.debug(" - status code: {}".format(response.status_code)) 519 uLogger.debug(" - reason: {}".format(response.reason)) 520 uLogger.debug(" - body length: {}".format(len(response.text))) 521 uLogger.debug(" - headers:\n{}".format(response.headers)) 522 523 # Server returns some headers: 524 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 525 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 526 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 527 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 528 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 529 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 530 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 531 sleep(rateLimitWait) 532 533 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 534 if 400 <= response.status_code < 500: 535 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 536 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 537 counter = retry + 1 538 539 if 500 <= response.status_code < 600: 540 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 541 uLogger.debug(" - not oK, {}".format(errMsg)) 542 counter += 1 543 544 if counter <= retry: 545 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 546 sleep(pause) 547 548 responseJSON = self._ParseJSON(rawData=response.text) 549 550 if errMsg: 551 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 552 uLogger.error(" - not oK, {}".format(errMsg)) 553 554 return responseJSON 555 556 def _IUpdater(self, iType: str) -> tuple: 557 """ 558 Request instrument by type from server. See available API methods for instruments: 559 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 560 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 561 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 562 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 563 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 564 565 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 566 :return: tuple with iType name and list of available instruments of current type for defined user token. 567 """ 568 result = [] 569 570 if iType in TKS_INSTRUMENTS: 571 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 572 573 # all instruments have the same body in API v2 requests: 574 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 575 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 576 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 577 578 return iType, result 579 580 def _IWrapper(self, kwargs): 581 """ 582 Wrapper runs instrument's update method `_IUpdater()`. 583 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 584 """ 585 return self._IUpdater(**kwargs) 586 587 def Listing(self) -> dict: 588 """ 589 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 590 591 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 592 """ 593 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 594 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 595 596 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 597 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 598 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 599 600 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 601 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 602 poolUpdater.close() 603 604 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 605 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 606 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 607 608 # calculate minimum price increment (step) for all instruments and set up instrument's type: 609 for iType in iList.keys(): 610 for ticker in iList[iType]: 611 iList[iType][ticker]["type"] = iType 612 613 if "minPriceIncrement" in iList[iType][ticker].keys(): 614 iList[iType][ticker]["step"] = NanoToFloat( 615 iList[iType][ticker]["minPriceIncrement"]["units"], 616 iList[iType][ticker]["minPriceIncrement"]["nano"], 617 ) 618 619 else: 620 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 621 622 return iList 623 624 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 625 """ 626 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 627 628 See also: `DumpInstruments()`, `Listing()`. 629 630 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 631 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 632 """ 633 if self.iListDumpFile is None or not self.iListDumpFile: 634 uLogger.error("Output name of dump file must be defined!") 635 raise Exception("Filename required") 636 637 if not self.iList or forceUpdate: 638 self.iList = self.Listing() 639 640 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 641 642 # Save as XLSX with separated sheets for every type of instruments: 643 with pd.ExcelWriter( 644 path=xlsxDumpFile, 645 date_format=TKS_DATE_FORMAT, 646 datetime_format=TKS_DATE_TIME_FORMAT, 647 mode="w", 648 ) as writer: 649 for iType in TKS_INSTRUMENTS: 650 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 651 df = df[sorted(df)] # sorted by column names 652 df = df.applymap( 653 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 654 na_action="ignore", 655 ) # converting numbers from nano-type to float in every cell 656 df.to_excel( 657 writer, 658 sheet_name=iType, 659 encoding="UTF-8", 660 freeze_panes=(1, 1), 661 ) # saving as XLSX-file with freeze first row and column as headers 662 663 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 664 665 def DumpInstruments(self, forceUpdate: bool = True) -> str: 666 """ 667 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 668 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 669 670 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 671 672 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 673 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 674 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 675 """ 676 if self.iListDumpFile is None or not self.iListDumpFile: 677 uLogger.error("Output name of dump file must be defined!") 678 raise Exception("Filename required") 679 680 if not self.iList or forceUpdate: 681 self.iList = self.Listing() 682 683 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 684 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 685 fH.write(jsonDump) 686 687 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 688 689 return jsonDump 690 691 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 692 """ 693 Show information about one instrument defined by json data and prints it in Markdown format. 694 695 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 696 697 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 698 :param show: if `True` then also printing information about instrument and its current price. 699 :return: multilines text in Markdown format with information about one instrument. 700 """ 701 splitLine = "| | |\n" 702 infoText = "" 703 704 if iJSON is not None and iJSON and isinstance(iJSON, dict): 705 info = [ 706 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 707 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 708 "| Parameters | Values |\n", 709 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 710 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 711 "| Full name: | {:<54} |\n".format(iJSON["name"]), 712 ] 713 714 if "sector" in iJSON.keys() and iJSON["sector"]: 715 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 716 717 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 718 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 719 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 720 ))) 721 722 info.extend([ 723 splitLine, 724 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 725 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 726 ]) 727 728 if "isin" in iJSON.keys() and iJSON["isin"]: 729 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 730 731 if "classCode" in iJSON.keys(): 732 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 733 734 info.extend([ 735 splitLine, 736 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 737 splitLine, 738 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 739 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 740 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 741 ]) 742 743 if iJSON["figi"]: 744 self.figi = iJSON["figi"] 745 iJSON = iJSON | self.RequestTradingStatus() 746 747 info.extend([ 748 splitLine, 749 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 750 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 751 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 752 ]) 753 754 info.append(splitLine) 755 756 if "type" in iJSON.keys() and iJSON["type"]: 757 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 758 759 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 760 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 761 762 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 763 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 764 765 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 766 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 767 768 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 769 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 770 771 if "focusType" in iJSON.keys() and iJSON["focusType"]: 772 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 773 774 if "assetType" in iJSON.keys() and iJSON["assetType"]: 775 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 776 777 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 778 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 779 780 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 781 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 782 783 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 784 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 785 786 if "currency" in iJSON.keys(): 787 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 788 789 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 790 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 791 792 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 793 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 794 795 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 796 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 799 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 800 801 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 802 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 803 804 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 805 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 806 807 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 808 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 809 810 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 811 info.append("| Perpetual bond: | Yes |\n") 812 813 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 814 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 815 816 iExt = None 817 if iJSON["type"] == "Bonds": 818 info.extend([ 819 splitLine, 820 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 821 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 822 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 823 iJSON["nominal"]["currency"], 824 )), 825 ]) 826 827 if "floatingCouponFlag" in iJSON.keys(): 828 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 829 830 if "amortizationFlag" in iJSON.keys(): 831 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 832 833 info.append(splitLine) 834 835 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 836 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 837 838 if iJSON["figi"]: 839 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 840 841 info.extend([ 842 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 843 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 844 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 845 ]) 846 847 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 848 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 849 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 850 iJSON["aciValue"]["currency"] 851 ))) 852 853 if "currentPrice" in iJSON.keys(): 854 info.append(splitLine) 855 856 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 857 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 858 859 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 860 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 861 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 862 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 863 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 864 865 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 866 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 867 868 info.extend([ 869 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 874 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 875 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 876 )), 877 "| Changes between last deal price and last close | {:<54} |\n".format( 878 "{:.2f}%{}".format( 879 iJSON["currentPrice"]["changes"], 880 " ({}{:.2f} {})".format( 881 "+" if bondChangesDelta > 0 else "", 882 bondChangesDelta, 883 aciCurrency 884 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 885 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 886 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 887 currency 888 ), 889 ) 890 ), 891 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 ]) 906 907 if "lot" in iJSON.keys(): 908 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 909 910 if "step" in iJSON.keys() and iJSON["step"] != 0: 911 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 912 913 # Add bond payment calendar: 914 if iJSON["type"] == "Bonds": 915 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 916 info.extend(["\n", strCalendar]) 917 918 infoText += "".join(info) 919 920 if show: 921 uLogger.info("{}".format(infoText)) 922 923 else: 924 uLogger.debug("{}".format(infoText)) 925 926 if self.infoFile is not None: 927 with open(self.infoFile, "w", encoding="UTF-8") as fH: 928 fH.write(infoText) 929 930 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 931 932 return infoText 933 934 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 935 """ 936 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 937 938 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 939 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 940 :return: JSON formatted data with information about instrument. 941 """ 942 tickerJSON = {} 943 if self.moreDebug: 944 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 945 946 if not self.ticker: 947 uLogger.warning("self.ticker variable is not be empty!") 948 949 else: 950 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 951 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 952 raise Exception("Instrument not allowed") 953 954 if not self.iList: 955 self.iList = self.Listing() 956 957 if self.ticker in self.iList["Shares"].keys(): 958 tickerJSON = self.iList["Shares"][self.ticker] 959 if self.moreDebug: 960 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 961 962 elif self.ticker in self.iList["Currencies"].keys(): 963 tickerJSON = self.iList["Currencies"][self.ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Bonds"].keys(): 968 tickerJSON = self.iList["Bonds"][self.ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Etfs"].keys(): 973 tickerJSON = self.iList["Etfs"][self.ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Futures"].keys(): 978 tickerJSON = self.iList["Futures"][self.ticker] 979 if self.moreDebug: 980 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 981 982 if tickerJSON: 983 self.figi = tickerJSON["figi"] 984 985 if requestPrice: 986 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 987 988 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 989 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 990 991 else: 992 tickerJSON["currentPrice"]["changes"] = 0 993 994 if show: 995 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 996 997 else: 998 if show: 999 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1000 1001 return tickerJSON 1002 1003 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1004 """ 1005 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1006 1007 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1008 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1009 :return: JSON formatted data with information about instrument. 1010 """ 1011 figiJSON = {} 1012 if self.moreDebug: 1013 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1014 1015 if not self.figi: 1016 uLogger.warning("self.figi variable is not be empty!") 1017 1018 else: 1019 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1020 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1021 raise Exception("Instrument not allowed") 1022 1023 if not self.iList: 1024 self.iList = self.Listing() 1025 1026 for item in self.iList["Shares"].keys(): 1027 if self.figi == self.iList["Shares"][item]["figi"]: 1028 figiJSON = self.iList["Shares"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Currencies"].keys(): 1037 if self.figi == self.iList["Currencies"][item]["figi"]: 1038 figiJSON = self.iList["Currencies"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Bonds"].keys(): 1047 if self.figi == self.iList["Bonds"][item]["figi"]: 1048 figiJSON = self.iList["Bonds"][item] 1049 1050 if self.moreDebug: 1051 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Etfs"].keys(): 1057 if self.figi == self.iList["Etfs"][item]["figi"]: 1058 figiJSON = self.iList["Etfs"][item] 1059 1060 if self.moreDebug: 1061 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Futures"].keys(): 1067 if self.figi == self.iList["Futures"][item]["figi"]: 1068 figiJSON = self.iList["Futures"][item] 1069 1070 if self.moreDebug: 1071 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1072 1073 break 1074 1075 if figiJSON: 1076 self.figi = figiJSON["figi"] 1077 self.ticker = figiJSON["ticker"] 1078 1079 if requestPrice: 1080 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1081 1082 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1083 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1084 1085 else: 1086 figiJSON["currentPrice"]["changes"] = 0 1087 1088 if show: 1089 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1090 1091 else: 1092 if show: 1093 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1094 1095 return figiJSON 1096 1097 def GetCurrentPrices(self, show: bool = True) -> dict: 1098 """ 1099 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1100 `{"buy": [{"price": 1243.8, "quantity": 193}, 1101 {"price": 1244.0, "quantity": 168}, 1102 {"price": 1244.8, "quantity": 5}, 1103 {"price": 1245.0, "quantity": 61}, 1104 {"price": 1245.4, "quantity": 60}], 1105 "sell": [{"price": 1243.6, "quantity": 8}, 1106 {"price": 1242.6, "quantity": 10}, 1107 {"price": 1242.4, "quantity": 18}, 1108 {"price": 1242.2, "quantity": 50}, 1109 {"price": 1242.0, "quantity": 113}], 1110 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1111 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1112 - sell: list of dicts with Buyers prices, 1113 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1114 - quantity: volume value by current price in lots, 1115 - limitUp: current trade session limit price, maximum, 1116 - limitDown: current trade session limit price, minimum, 1117 - lastPrice: last deal price of the instrument, 1118 - closePrice: previous trade session close price of the instrument. 1119 1120 See also: `SearchByTicker()` and `SearchByFIGI()`. 1121 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1122 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1123 1124 :param show: if `True` then print DOM to log and console. 1125 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1126 If an error occurred then returns an empty record: 1127 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1128 """ 1129 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1130 1131 if self.depth < 1: 1132 uLogger.error("Depth of Market (DOM) must be >=1!") 1133 raise Exception("Incorrect value") 1134 1135 if not (self.ticker or self.figi): 1136 uLogger.error("self.ticker or self.figi variables must be defined!") 1137 raise Exception("Ticker or FIGI required") 1138 1139 if self.ticker and not self.figi: 1140 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1141 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1142 1143 if not self.ticker and self.figi: 1144 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1145 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1146 1147 if not self.figi: 1148 uLogger.error("FIGI is not defined!") 1149 raise Exception("Ticker or FIGI required") 1150 1151 else: 1152 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1153 1154 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1155 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1156 self.body = str({"figi": self.figi, "depth": self.depth}) 1157 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1158 1159 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1160 # list of dicts with sellers orders: 1161 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1162 1163 # list of dicts with buyers orders: 1164 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1165 1166 # max price of instrument at this time: 1167 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1168 1169 # min price of instrument at this time: 1170 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1171 1172 # last price of deal with instrument: 1173 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1174 1175 # last close price of instrument: 1176 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1177 1178 else: 1179 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1180 uLogger.debug("Server response: {}".format(pricesResponse)) 1181 1182 if show: 1183 if prices["buy"] or prices["sell"]: 1184 info = [ 1185 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1186 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1187 self.ticker, 1188 self.figi, 1189 self.depth, 1190 ), 1191 "-" * 60, "\n", 1192 " Orders of Buyers | Orders of Sellers\n", 1193 "-" * 60, "\n", 1194 " Sell prices (volumes) | Buy prices (volumes)\n", 1195 "-" * 60, "\n", 1196 ] 1197 1198 if not prices["buy"]: 1199 info.append(" | No orders!\n") 1200 sumBuy = 0 1201 1202 else: 1203 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1204 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1205 for item in maxMinSorted: 1206 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1207 1208 if not prices["sell"]: 1209 info.append("No orders! |\n") 1210 sumSell = 0 1211 1212 else: 1213 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1214 for item in prices["sell"]: 1215 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1216 1217 info.extend([ 1218 "-" * 60, "\n", 1219 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1220 "-" * 60, "\n", 1221 ]) 1222 1223 infoText = "".join(info) 1224 1225 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1226 1227 else: 1228 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1229 1230 return prices 1231 1232 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1233 """ 1234 This method get and show information about all available broker instruments for current user account. 1235 If `instrumentsFile` string is not empty then also save information to this file. 1236 1237 :param show: if `True` then print results to console, if `False` - print only to file. 1238 :return: multi-lines string with all available broker instruments 1239 """ 1240 if not self.iList: 1241 self.iList = self.Listing() 1242 1243 info = [ 1244 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1245 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1246 ] 1247 1248 # add instruments count by type: 1249 for iType in self.iList.keys(): 1250 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1251 1252 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1253 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1254 1255 # generating info tables with all instruments by type: 1256 for iType in self.iList.keys(): 1257 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1258 1259 for instrument in self.iList[iType].keys(): 1260 iName = self.iList[iType][instrument]["name"] # instrument's name 1261 if len(iName) > 57: 1262 iName = "{}...".format(iName[:54]) # right trim for a long string 1263 1264 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1265 self.iList[iType][instrument]["ticker"], 1266 iName, 1267 self.iList[iType][instrument]["figi"], 1268 self.iList[iType][instrument]["currency"], 1269 self.iList[iType][instrument]["lot"], 1270 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1271 )) 1272 1273 infoText = "".join(info) 1274 1275 if show: 1276 uLogger.info(infoText) 1277 1278 if self.instrumentsFile: 1279 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1280 fH.write(infoText) 1281 1282 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1283 1284 return infoText 1285 1286 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1287 """ 1288 This method search and show information about instruments by part of its ticker, FIGI or name. 1289 If `searchResultsFile` string is not empty then also save information to this file. 1290 1291 :param pattern: string with part of ticker, FIGI or instrument's name. 1292 :param show: if `True` then print results to console, if `False` - return list of result only. 1293 :return: list of dictionaries with all found instruments. 1294 """ 1295 if not self.iList: 1296 self.iList = self.Listing() 1297 1298 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1299 compiledPattern = re.compile(pattern, re.IGNORECASE) 1300 1301 for iType in self.iList: 1302 for instrument in self.iList[iType].values(): 1303 searchResult = compiledPattern.search(" ".join( 1304 [instrument["ticker"], instrument["figi"], instrument["name"]] 1305 )) 1306 1307 if searchResult: 1308 searchResults[iType][instrument["ticker"]] = instrument 1309 1310 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1311 info = [ 1312 "# Search results\n\n", 1313 "* **Search pattern:** [{}]\n".format(pattern), 1314 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1315 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1316 ] 1317 infoShort = info[:] 1318 1319 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1320 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1321 skippedLine = "| ... | ... | ... | ... |\n" 1322 1323 if resultsLen == 0: 1324 info.append("\nNo results\n") 1325 infoShort.append("\nNo results\n") 1326 uLogger.warning("No results. Try changing your search pattern.") 1327 1328 else: 1329 for iType in searchResults: 1330 iTypeValuesCount = len(searchResults[iType].values()) 1331 if iTypeValuesCount > 0: 1332 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1333 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1334 1335 for instrument in searchResults[iType].values(): 1336 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1337 instrument["type"], 1338 instrument["ticker"], 1339 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1340 instrument["figi"], 1341 )) 1342 1343 if iTypeValuesCount <= 5: 1344 infoShort.extend(info[-iTypeValuesCount:]) 1345 1346 else: 1347 infoShort.extend(info[-5:]) 1348 infoShort.append(skippedLine) 1349 1350 infoText = "".join(info) 1351 infoTextShort = "".join(infoShort) 1352 1353 if show: 1354 uLogger.info(infoTextShort) 1355 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1356 1357 if self.searchResultsFile: 1358 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1359 fH.write(infoText) 1360 1361 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1362 1363 return searchResults 1364 1365 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1366 """ 1367 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1368 1369 :param instruments: list of strings with tickers or FIGIs. 1370 :return: list with unique instrument FIGIs only. 1371 """ 1372 requestedInstruments = [] 1373 for iName in instruments: 1374 if iName not in self.aliases.keys(): 1375 if iName not in requestedInstruments: 1376 requestedInstruments.append(iName) 1377 1378 else: 1379 if iName not in requestedInstruments: 1380 if self.aliases[iName] not in requestedInstruments: 1381 requestedInstruments.append(self.aliases[iName]) 1382 1383 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1384 1385 onlyUniqueFIGIs = [] 1386 for iName in requestedInstruments: 1387 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1388 continue 1389 1390 self.ticker = iName 1391 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1392 1393 if not iData: 1394 self.ticker = "" 1395 self.figi = iName 1396 1397 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1398 1399 if not iData: 1400 self.figi = "" 1401 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1402 1403 if iData and iData["figi"] not in onlyUniqueFIGIs: 1404 onlyUniqueFIGIs.append(iData["figi"]) 1405 1406 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1407 1408 return onlyUniqueFIGIs 1409 1410 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1411 """ 1412 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1413 See limits: https://tinkoff.github.io/investAPI/limits/ 1414 If `pricesFile` string is not empty then also save information to this file. 1415 1416 :param instruments: list of strings with tickers or FIGIs. 1417 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1418 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1419 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1420 """ 1421 if instruments is None or not instruments: 1422 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1423 raise Exception("Ticker or FIGI required") 1424 1425 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1426 1427 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1428 1429 iList = [] # trying to get info and current prices about all unique instruments: 1430 for self.figi in onlyUniqueFIGIs: 1431 iData = self.SearchByFIGI(requestPrice=True) 1432 iList.append(iData) 1433 1434 self.ShowListOfPrices(iList, show) 1435 1436 return iList 1437 1438 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1439 """ 1440 Show table contains current prices of given instruments. 1441 1442 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1443 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1444 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1445 :return: multilines text in Markdown format as a table contains current prices. 1446 """ 1447 infoText = "" 1448 1449 if show or self.pricesFile: 1450 info = [ 1451 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1452 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1453 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1454 ] 1455 1456 for item in iList: 1457 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1458 item["ticker"], 1459 item["figi"], 1460 item["type"], 1461 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1462 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1463 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1464 "{} / {}".format( 1465 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1466 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1467 ), 1468 "{} / {}".format( 1469 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1470 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1471 ), 1472 item["currency"], 1473 )) 1474 1475 infoText = "".join(info) 1476 1477 if show: 1478 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1479 1480 if self.pricesFile: 1481 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1482 fH.write(infoText) 1483 1484 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1485 1486 return infoText 1487 1488 def RequestTradingStatus(self) -> dict: 1489 """ 1490 Requesting trading status for the instrument defined by `figi` variable. 1491 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1492 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1493 1494 :return: dictionary with trading status attributes. Response example: 1495 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1496 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1497 """ 1498 if self.figi is None or not self.figi: 1499 uLogger.error("Variable `figi` must be defined for using this method!") 1500 raise Exception("FIGI required") 1501 1502 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1503 1504 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1505 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1506 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1507 1508 if self.moreDebug: 1509 uLogger.debug("Records about current trading status successfully received") 1510 1511 return tradingStatus 1512 1513 def RequestPortfolio(self) -> dict: 1514 """ 1515 Requesting actual user's portfolio for current `accountId`. 1516 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1517 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1518 1519 :return: dictionary with user's portfolio. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1529 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1530 1531 if self.moreDebug: 1532 uLogger.debug("Records about user's portfolio successfully received") 1533 1534 return rawPortfolio 1535 1536 def RequestPositions(self) -> dict: 1537 """ 1538 Requesting open positions by currencies and instruments for current `accountId`. 1539 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1540 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1541 1542 :return: dictionary with open positions by instruments. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1552 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1553 1554 if self.moreDebug: 1555 uLogger.debug("Records about current open positions successfully received") 1556 1557 return rawPositions 1558 1559 def RequestPendingOrders(self) -> list: 1560 """ 1561 Requesting current actual pending orders for current `accountId`. 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1564 1565 :return: list of dictionaries with pending orders. 1566 """ 1567 if self.accountId is None or not self.accountId: 1568 uLogger.error("Variable `accountId` must be defined for using this method!") 1569 raise Exception("Account ID required") 1570 1571 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1572 1573 self.body = str({"accountId": self.accountId}) 1574 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1575 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1576 1577 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1578 1579 return rawOrders 1580 1581 def RequestStopOrders(self) -> list: 1582 """ 1583 Requesting current actual stop orders for current `accountId`. 1584 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1585 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1586 1587 :return: list of dictionaries with stop orders. 1588 """ 1589 if self.accountId is None or not self.accountId: 1590 uLogger.error("Variable `accountId` must be defined for using this method!") 1591 raise Exception("Account ID required") 1592 1593 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1594 1595 self.body = str({"accountId": self.accountId}) 1596 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1597 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1598 1599 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1600 1601 return rawStopOrders 1602 1603 def Overview(self, show: bool = False, details: str = "full") -> dict: 1604 """ 1605 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1606 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1607 are defined then also save information to file. 1608 1609 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1610 many requests about the state of the portfolio, and then, based on the received data, a large number 1611 of calculation and statistics are collected. 1612 1613 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1614 :param details: how detailed should the information be? You should specify one of strings: 1615 `full` - shows full available information about portfolio status (by default), 1616 `positions` - shows only open positions, 1617 `digest` - show a short digest of the portfolio status, 1618 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1619 `orders` - shows only sections of open limits and stop orders. 1620 :return: dictionary with client's raw portfolio and some statistics. 1621 """ 1622 if self.accountId is None or not self.accountId: 1623 uLogger.error("Variable `accountId` must be defined for using this method!") 1624 raise Exception("Account ID required") 1625 1626 view = { 1627 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1628 "headers": {}, # list of dictionaries, response headers without "positions" section 1629 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1630 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1631 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1632 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1633 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1634 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1635 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1636 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1637 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1638 }, 1639 "stat": { # --- some statistics calculated using "raw" sections: 1640 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1641 "availableRUB": 0., # available rubles (without other currencies) 1642 "blockedRUB": 0., # blocked sum in Russian Rouble 1643 "totalChangesRUB": 0., # changes for all open trades in RUB 1644 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1645 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1646 "sharesCostRUB": 0., # costs of all shares in RUB 1647 "bondsCostRUB": 0., # costs of all bonds in RUB 1648 "etfsCostRUB": 0., # costs of all etfs in RUB 1649 "futuresCostRUB": 0., # costs of all futures in RUB 1650 "Currencies": [], # list of dictionaries of all currencies statistics 1651 "Shares": [], # list of dictionaries of all shares statistics 1652 "Bonds": [], # list of dictionaries of all bonds statistics 1653 "Etfs": [], # list of dictionaries of all etfs statistics 1654 "Futures": [], # list of dictionaries of all futures statistics 1655 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1656 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1657 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1658 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1659 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1660 }, 1661 "analytics": { # --- some analytics of portfolio: 1662 "distrByAssets": {}, # portfolio distribution by assets 1663 "distrByCompanies": {}, # portfolio distribution by companies 1664 "distrBySectors": {}, # portfolio distribution by sectors 1665 "distrByCurrencies": {}, # portfolio distribution by currencies 1666 "distrByCountries": {}, # portfolio distribution by countries 1667 } 1668 } 1669 1670 details = details.lower() 1671 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1672 if details not in availableDetails: 1673 details = "full" 1674 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1675 1676 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1677 1678 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1679 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1680 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1681 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1682 1683 # save response headers without "positions" section: 1684 for key in portfolioResponse.keys(): 1685 if key != "positions": 1686 view["raw"]["headers"][key] = portfolioResponse[key] 1687 1688 else: 1689 continue 1690 1691 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1692 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1693 for item in portfolioResponse["positions"]: 1694 if item["instrumentType"] == "currency": 1695 self.figi = item["figi"] 1696 curr = self.SearchByFIGI(requestPrice=False) 1697 1698 # current price of currency in RUB: 1699 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1700 "name": curr["name"], 1701 "currentPrice": NanoToFloat( 1702 item["currentPrice"]["units"], 1703 item["currentPrice"]["nano"] 1704 ), 1705 } 1706 1707 view["raw"]["Currencies"].append(item) 1708 1709 elif item["instrumentType"] == "share": 1710 view["raw"]["Shares"].append(item) 1711 1712 elif item["instrumentType"] == "bond": 1713 view["raw"]["Bonds"].append(item) 1714 1715 elif item["instrumentType"] == "etf": 1716 view["raw"]["Etfs"].append(item) 1717 1718 elif item["instrumentType"] == "futures": 1719 view["raw"]["Futures"].append(item) 1720 1721 else: 1722 continue 1723 1724 # how many volume of currencies (by ISO currency name) are blocked: 1725 for item in view["raw"]["positions"]["blocked"]: 1726 blocked = NanoToFloat(item["units"], item["nano"]) 1727 if blocked > 0: 1728 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1729 1730 # how many volume of instruments (by FIGI) are blocked: 1731 for item in view["raw"]["positions"]["securities"]: 1732 blocked = int(item["blocked"]) 1733 if blocked > 0: 1734 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1735 1736 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1737 1738 if "rub" in allBlocked.keys(): 1739 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1740 1741 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1742 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1743 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1744 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1745 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1746 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1747 view["stat"]["portfolioCostRUB"] = sum([ 1748 view["stat"]["allCurrenciesCostRUB"], 1749 view["stat"]["sharesCostRUB"], 1750 view["stat"]["bondsCostRUB"], 1751 view["stat"]["etfsCostRUB"], 1752 view["stat"]["futuresCostRUB"], 1753 ]) 1754 1755 # --- calculating some portfolio statistics: 1756 byComp = {} # distribution by companies 1757 bySect = {} # distribution by sectors 1758 byCurr = {} # distribution by currencies (include RUB) 1759 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1760 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1761 1762 for item in portfolioResponse["positions"]: 1763 self.figi = item["figi"] 1764 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1765 1766 if instrument: 1767 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1768 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1769 1770 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1771 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1772 1773 else: 1774 blocked = 0 1775 1776 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1777 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1778 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1779 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1780 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1781 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1782 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1783 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1784 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1785 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1786 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1787 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1788 1789 statData = { 1790 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1791 "ticker": instrument["ticker"], # ticker by FIGI 1792 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1793 "volume": volume, # available volume of instrument 1794 "lots": lots, # volume in lots of instrument 1795 "direction": direction, # direction of an instrument's position: short or long 1796 "blocked": blocked, # blocked volume of currency or instrument 1797 "currentPrice": curPrice, # current instrument's price in basic asset 1798 "average": average, # current average position price 1799 "cost": cost, # current cost of all volume of instrument in basic asset 1800 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1801 "costRUB": costRUB, # cost of instrument in ruble 1802 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1803 "profit": profit, # expected profit at current moment 1804 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1805 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1806 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1807 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1808 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1809 "step": instrument["step"], # minimum price increment 1810 } 1811 1812 # adding distribution by unique countries: 1813 if statData["country"] not in byCountry.keys(): 1814 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1815 1816 else: 1817 byCountry[statData["country"]]["cost"] += costRUB 1818 byCountry[statData["country"]]["percent"] += percentCostRUB 1819 1820 if item["instrumentType"] != "currency": 1821 # adding distribution by unique companies: 1822 if statData["name"]: 1823 if statData["name"] not in byComp.keys(): 1824 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1825 1826 else: 1827 byComp[statData["name"]]["cost"] += costRUB 1828 byComp[statData["name"]]["percent"] += percentCostRUB 1829 1830 # adding distribution by unique sectors: 1831 if statData["sector"] not in bySect.keys(): 1832 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1833 1834 else: 1835 bySect[statData["sector"]]["cost"] += costRUB 1836 bySect[statData["sector"]]["percent"] += percentCostRUB 1837 1838 # adding distribution by unique currencies: 1839 if currency not in byCurr.keys(): 1840 byCurr[currency] = { 1841 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1842 "cost": costRUB, 1843 "percent": percentCostRUB 1844 } 1845 1846 else: 1847 byCurr[currency]["cost"] += costRUB 1848 byCurr[currency]["percent"] += percentCostRUB 1849 1850 # saving statistics for every instrument: 1851 if item["instrumentType"] == "currency": 1852 view["stat"]["Currencies"].append(statData) 1853 1854 # update dict with free funds for trading (total - blocked) by currencies 1855 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1856 view["stat"]["funds"][currency] = { 1857 "total": volume, 1858 "totalCostRUB": costRUB, # total volume cost in rubles 1859 "free": volume - blocked, 1860 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1861 } 1862 1863 elif item["instrumentType"] == "share": 1864 view["stat"]["Shares"].append(statData) 1865 1866 elif item["instrumentType"] == "bond": 1867 view["stat"]["Bonds"].append(statData) 1868 1869 elif item["instrumentType"] == "etf": 1870 view["stat"]["Etfs"].append(statData) 1871 1872 elif item["instrumentType"] == "Futures": 1873 view["stat"]["Futures"].append(statData) 1874 1875 else: 1876 continue 1877 1878 # total changes in Russian Ruble: 1879 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1880 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1881 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1882 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1883 view["stat"]["funds"]["rub"] = { 1884 "total": view["stat"]["availableRUB"], 1885 "totalCostRUB": view["stat"]["availableRUB"], 1886 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1887 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1888 } 1889 1890 # --- pending orders sector data: 1891 uniquePendingOrders = [] 1892 uniquePendingOrdersFIGIs = [] 1893 for item in view["raw"]["orders"]: 1894 if item["figi"] not in uniquePendingOrdersFIGIs: 1895 uniquePendingOrdersFIGIs.append(item["figi"]) 1896 uniquePendingOrders.append(item) 1897 1898 for item in uniquePendingOrders: 1899 self.figi = item["figi"] 1900 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1901 1902 if instrument: 1903 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1904 orderType = TKS_ORDER_TYPES[item["orderType"]] 1905 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1906 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1907 1908 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1909 if item["direction"] == "ORDER_DIRECTION_BUY": 1910 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1911 1912 else: 1913 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1914 1915 # requested price for order execution: 1916 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1917 1918 # necessary changes in percent to reach target from current price: 1919 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1920 1921 view["stat"]["orders"].append({ 1922 "orderID": item["orderId"], # orderId number parameter of current order 1923 "figi": item["figi"], # FIGI identification 1924 "ticker": instrument["ticker"], # ticker name by FIGI 1925 "lotsRequested": item["lotsRequested"], # requested lots value 1926 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1927 "currentPrice": lastPrice, # current instrument's price for defined action 1928 "targetPrice": target, # requested price for order execution in base currency 1929 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1930 "percentChanges": changes, # changes in percent to target from current price 1931 "currency": item["currency"], # instrument's currency name 1932 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1933 "type": orderType, # type of order from TKS_ORDER_TYPES 1934 "status": orderState, # order status from TKS_ORDER_STATES 1935 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1936 }) 1937 1938 # --- stop orders sector data: 1939 uniqueStopOrders = [] 1940 uniqueStopOrdersFIGIs = [] 1941 for item in view["raw"]["stopOrders"]: 1942 if item["figi"] not in uniqueStopOrdersFIGIs: 1943 uniqueStopOrdersFIGIs.append(item["figi"]) 1944 uniqueStopOrders.append(item) 1945 1946 for item in uniqueStopOrders: 1947 self.figi = item["figi"] 1948 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1949 1950 if instrument: 1951 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1952 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1953 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1954 1955 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1956 if "expirationTime" in item.keys(): 1957 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1958 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1959 1960 else: 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1962 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1963 1964 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1965 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1966 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1967 1968 else: 1969 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1970 1971 # requested price when stop-order executed: 1972 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1973 1974 # price for limit-order, set up when stop-order executed: 1975 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1976 1977 # necessary changes in percent to reach target from current price: 1978 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1979 1980 view["stat"]["stopOrders"].append({ 1981 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1982 "figi": item["figi"], # FIGI identification 1983 "ticker": instrument["ticker"], # ticker name by FIGI 1984 "lotsRequested": item["lotsRequested"], # requested lots value 1985 "currentPrice": lastPrice, # current instrument's price for defined action 1986 "targetPrice": target, # requested price for stop-order execution in base currency 1987 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1988 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1989 "percentChanges": changes, # changes in percent to target from current price 1990 "currency": item["currency"], # instrument's currency name 1991 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1992 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1993 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1994 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1995 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1996 }) 1997 1998 # --- calculating data for analytics section: 1999 # portfolio distribution by assets: 2000 view["analytics"]["distrByAssets"] = { 2001 "Ruble": { 2002 "uniques": 1, 2003 "cost": view["stat"]["availableRUB"], 2004 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 "Currencies": { 2007 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2008 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2009 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2010 }, 2011 "Shares": { 2012 "uniques": len(view["stat"]["Shares"]), 2013 "cost": view["stat"]["sharesCostRUB"], 2014 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2015 }, 2016 "Bonds": { 2017 "uniques": len(view["stat"]["Bonds"]), 2018 "cost": view["stat"]["bondsCostRUB"], 2019 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2020 }, 2021 "Etfs": { 2022 "uniques": len(view["stat"]["Etfs"]), 2023 "cost": view["stat"]["etfsCostRUB"], 2024 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2025 }, 2026 "Futures": { 2027 "uniques": len(view["stat"]["Futures"]), 2028 "cost": view["stat"]["futuresCostRUB"], 2029 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2030 }, 2031 } 2032 2033 # portfolio distribution by companies: 2034 view["analytics"]["distrByCompanies"]["All money cash"] = { 2035 "ticker": "", 2036 "cost": view["stat"]["allCurrenciesCostRUB"], 2037 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 } 2039 view["analytics"]["distrByCompanies"].update(byComp) 2040 2041 # portfolio distribution by sectors: 2042 view["analytics"]["distrBySectors"]["All money cash"] = { 2043 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2044 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2045 } 2046 view["analytics"]["distrBySectors"].update(bySect) 2047 2048 # portfolio distribution by currencies: 2049 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2050 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2051 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2052 2053 view["analytics"]["distrByCurrencies"].update(byCurr) 2054 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2055 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2056 2057 # portfolio distribution by countries: 2058 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2059 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2060 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2061 2062 view["analytics"]["distrByCountries"].update(byCountry) 2063 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2065 2066 # --- Prepare text statistics overview in human-readable: 2067 if show: 2068 # Whatever the value `details`, header not changes: 2069 info = [ 2070 "# Client's portfolio\n\n", 2071 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2072 "* **Account ID:** [{}]\n".format(self.accountId), 2073 ] 2074 2075 if details in ["full", "positions", "digest"]: 2076 info.extend([ 2077 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2078 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2079 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2080 view["stat"]["totalChangesRUB"], 2081 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2082 view["stat"]["totalChangesPercentRUB"], 2083 ), 2084 ]) 2085 2086 if details in ["full", "positions"]: 2087 info.extend([ 2088 "## Open positions\n\n", 2089 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2090 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2091 "| Ruble | {:>31} | | | | | |\n".format( 2092 "{:.2f} ({:.2f}) rub".format( 2093 view["stat"]["availableRUB"], 2094 view["stat"]["blockedRUB"], 2095 ) 2096 ) 2097 ]) 2098 2099 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2100 return [ 2101 "| | | | | | | |\n", 2102 "| {:<27} | | | | | {:>19} | |\n".format( 2103 noTradeStr if noTradeStr else typeStr, 2104 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2105 ), 2106 ] 2107 2108 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2109 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2110 "{} [{}]".format(data["ticker"], data["figi"]), 2111 "{:.2f} ({:.2f}) {}".format( 2112 data["volume"], 2113 data["blocked"], 2114 data["currency"], 2115 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2116 data["volume"], 2117 data["blocked"], 2118 ), 2119 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2120 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2121 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2122 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2123 "{}{:.2f} {} ({}{:.2f}%)".format( 2124 "+" if data["profit"] > 0 else "", 2125 data["profit"], data["baseCurrencyName"], 2126 "+" if data["percentProfit"] > 0 else "", 2127 data["percentProfit"], 2128 ), 2129 ) 2130 2131 # --- Show currencies section: 2132 if view["stat"]["Currencies"]: 2133 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2134 for item in view["stat"]["Currencies"]: 2135 info.append(_InfoStr(item, showCurrencyName=True)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2139 2140 # --- Show shares section: 2141 if view["stat"]["Shares"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2143 2144 for item in view["stat"]["Shares"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2149 2150 # --- Show bonds section: 2151 if view["stat"]["Bonds"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2153 2154 for item in view["stat"]["Bonds"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2159 2160 # --- Show etfs section: 2161 if view["stat"]["Etfs"]: 2162 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2163 2164 for item in view["stat"]["Etfs"]: 2165 info.append(_InfoStr(item)) 2166 2167 else: 2168 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2169 2170 # --- Show futures section: 2171 if view["stat"]["Futures"]: 2172 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2173 2174 for item in view["stat"]["Futures"]: 2175 info.append(_InfoStr(item)) 2176 2177 else: 2178 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2179 2180 if details in ["full", "orders"]: 2181 # --- Show pending orders section: 2182 if view["stat"]["orders"]: 2183 info.extend([ 2184 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2185 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2186 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2187 ]) 2188 2189 for item in view["stat"]["orders"]: 2190 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2191 "{} [{}]".format(item["ticker"], item["figi"]), 2192 item["orderID"], 2193 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2194 "{} {} ({}{:.2f}%)".format( 2195 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2196 item["baseCurrencyName"], 2197 "+" if item["percentChanges"] > 0 else "", 2198 float(item["percentChanges"]), 2199 ), 2200 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2201 item["action"], 2202 item["type"], 2203 item["date"], 2204 )) 2205 2206 else: 2207 info.append("\n## Total pending limit-orders: 0\n") 2208 2209 # --- Show stop orders section: 2210 if view["stat"]["stopOrders"]: 2211 info.extend([ 2212 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2213 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2214 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2215 ]) 2216 2217 for item in view["stat"]["stopOrders"]: 2218 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2219 "{} [{}]".format(item["ticker"], item["figi"]), 2220 item["orderID"], 2221 item["lotsRequested"], 2222 "{} {} ({}{:.2f}%)".format( 2223 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2224 item["baseCurrencyName"], 2225 "+" if item["percentChanges"] > 0 else "", 2226 float(item["percentChanges"]), 2227 ), 2228 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2229 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2230 item["action"], 2231 item["type"], 2232 item["expType"], 2233 item["createDate"], 2234 item["expDate"], 2235 )) 2236 2237 else: 2238 info.append("\n## Total stop-orders: 0\n") 2239 2240 if details in ["full", "analytics"]: 2241 # -- Show analytics section: 2242 if view["stat"]["portfolioCostRUB"] > 0: 2243 info.extend([ 2244 "\n# Analytics\n" 2245 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2246 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2247 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2248 view["stat"]["totalChangesRUB"], 2249 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2250 view["stat"]["totalChangesPercentRUB"], 2251 ), 2252 "\n## Portfolio distribution by assets\n" 2253 "\n| Type | Uniques | Percent | Current cost |\n", 2254 "|------------|---------|---------|--------------------|\n", 2255 ]) 2256 2257 for key in view["analytics"]["distrByAssets"].keys(): 2258 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2259 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2260 key, 2261 view["analytics"]["distrByAssets"][key]["uniques"], 2262 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2263 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2264 )) 2265 2266 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2267 info.extend([ 2268 "\n## Portfolio distribution by companies\n" 2269 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2270 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2271 ]) 2272 2273 for company in view["analytics"]["distrByCompanies"].keys(): 2274 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2275 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2276 info.append("| {} | {:<7} | {:<18} |\n".format( 2277 "{}{}{}".format( 2278 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2279 company, 2280 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2281 ), 2282 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2283 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2284 )) 2285 2286 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2287 info.extend([ 2288 "\n## Portfolio distribution by sectors\n" 2289 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2290 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2291 ]) 2292 2293 for sector in view["analytics"]["distrBySectors"].keys(): 2294 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2295 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2296 sector, 2297 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2298 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2299 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2300 )) 2301 2302 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2303 info.extend([ 2304 "\n## Portfolio distribution by currencies\n" 2305 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2306 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2307 ]) 2308 2309 for curr in view["analytics"]["distrByCurrencies"].keys(): 2310 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2311 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2312 info.append("| {} | {:<7} | {:<18} |\n".format( 2313 "[{}] {}{}".format( 2314 curr, 2315 view["analytics"]["distrByCurrencies"][curr]["name"], 2316 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2317 ), 2318 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2319 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2320 )) 2321 2322 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2323 info.extend([ 2324 "\n## Portfolio distribution by countries\n" 2325 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2326 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2327 ]) 2328 2329 for country in view["analytics"]["distrByCountries"].keys(): 2330 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2331 nameLen = len(country) 2332 info.append("| {} | {:<7} | {:<18} |\n".format( 2333 "{}{}".format( 2334 country, 2335 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2336 ), 2337 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2338 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2339 )) 2340 2341 infoText = "".join(info) 2342 2343 uLogger.info(infoText) 2344 2345 if details == "full" and self.overviewFile: 2346 filename = self.overviewFile 2347 2348 elif details == "digest" and self.overviewDigestFile: 2349 filename = self.overviewDigestFile 2350 2351 elif details == "positions" and self.overviewPositionsFile: 2352 filename = self.overviewPositionsFile 2353 2354 elif details == "orders" and self.overviewOrdersFile: 2355 filename = self.overviewOrdersFile 2356 2357 elif details == "analytics" and self.overviewAnalyticsFile: 2358 filename = self.overviewAnalyticsFile 2359 2360 else: 2361 filename = "" 2362 2363 if filename: 2364 with open(filename, "w", encoding="UTF-8") as fH: 2365 fH.write(infoText) 2366 2367 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2368 2369 return view 2370 2371 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2372 """ 2373 Returns history operations between two given dates for current `accountId`. 2374 If `reportFile` string is not empty then also save human-readable report. 2375 Shows some statistical data of closed positions. 2376 2377 :param start: see docstring in `GetDatesAsString()` method 2378 :param end: see docstring in `GetDatesAsString()` method 2379 :param show: if `True` then also prints all records to the console. 2380 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2381 :return: original list of dictionaries with history of deals records from API ("operations" key): 2382 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2383 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2384 """ 2385 if self.accountId is None or not self.accountId: 2386 uLogger.error("Variable `accountId` must be defined for using this method!") 2387 raise Exception("Account ID required") 2388 2389 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2390 2391 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2392 2393 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2394 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2395 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2396 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2397 customStat = {} # custom statistics in additional to responseJSON 2398 2399 # --- output report in human-readable format: 2400 if show or self.reportFile: 2401 splitLine1 = "| | | | | |\n" # Summary section 2402 splitLine2 = "| | | | | | | | |\n" # Operations section 2403 nextDay = "" 2404 2405 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2406 2407 if len(ops) > 0: 2408 customStat = { 2409 "opsCount": 0, # total operations count 2410 "buyCount": 0, # buy operations 2411 "sellCount": 0, # sell operations 2412 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2413 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2414 "payIn": {"rub": 0.}, # Deposit brokerage account 2415 "payOut": {"rub": 0.}, # Withdrawals 2416 "divs": {"rub": 0.}, # Dividends income 2417 "coupons": {"rub": 0.}, # Coupon's income 2418 "brokerCom": {"rub": 0.}, # Service commissions 2419 "serviceCom": {"rub": 0.}, # Service commissions 2420 "marginCom": {"rub": 0.}, # Margin commissions 2421 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2422 } 2423 2424 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2425 for item in ops: 2426 if item["state"] == "OPERATION_STATE_EXECUTED": 2427 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2428 2429 # count buy operations: 2430 if "_BUY" in item["operationType"]: 2431 customStat["buyCount"] += 1 2432 2433 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2434 customStat["buyTotal"][item["payment"]["currency"]] += payment 2435 2436 else: 2437 customStat["buyTotal"][item["payment"]["currency"]] = payment 2438 2439 # count sell operations: 2440 elif "_SELL" in item["operationType"]: 2441 customStat["sellCount"] += 1 2442 2443 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2444 customStat["sellTotal"][item["payment"]["currency"]] += payment 2445 2446 else: 2447 customStat["sellTotal"][item["payment"]["currency"]] = payment 2448 2449 # count incoming operations: 2450 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2451 if item["payment"]["currency"] in customStat["payIn"].keys(): 2452 customStat["payIn"][item["payment"]["currency"]] += payment 2453 2454 else: 2455 customStat["payIn"][item["payment"]["currency"]] = payment 2456 2457 # count withdrawals operations: 2458 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2459 if item["payment"]["currency"] in customStat["payOut"].keys(): 2460 customStat["payOut"][item["payment"]["currency"]] += payment 2461 2462 else: 2463 customStat["payOut"][item["payment"]["currency"]] = payment 2464 2465 # count dividends income: 2466 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2467 if item["payment"]["currency"] in customStat["divs"].keys(): 2468 customStat["divs"][item["payment"]["currency"]] += payment 2469 2470 else: 2471 customStat["divs"][item["payment"]["currency"]] = payment 2472 2473 # count coupon's income: 2474 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2475 if item["payment"]["currency"] in customStat["coupons"].keys(): 2476 customStat["coupons"][item["payment"]["currency"]] += payment 2477 2478 else: 2479 customStat["coupons"][item["payment"]["currency"]] = payment 2480 2481 # count broker commissions: 2482 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2483 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2484 customStat["brokerCom"][item["payment"]["currency"]] += payment 2485 2486 else: 2487 customStat["brokerCom"][item["payment"]["currency"]] = payment 2488 2489 # count service commissions: 2490 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2491 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2492 customStat["serviceCom"][item["payment"]["currency"]] += payment 2493 2494 else: 2495 customStat["serviceCom"][item["payment"]["currency"]] = payment 2496 2497 # count margin commissions: 2498 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2499 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2500 customStat["marginCom"][item["payment"]["currency"]] += payment 2501 2502 else: 2503 customStat["marginCom"][item["payment"]["currency"]] = payment 2504 2505 # count withholding taxes: 2506 elif "_TAX" in item["operationType"]: 2507 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2508 customStat["allTaxes"][item["payment"]["currency"]] += payment 2509 2510 else: 2511 customStat["allTaxes"][item["payment"]["currency"]] = payment 2512 2513 else: 2514 continue 2515 2516 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2517 2518 # --- view "Actions" lines: 2519 info.extend([ 2520 "| Report sections | | | | |\n", 2521 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2522 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2523 "| | Buy: {:<22} | {:<28} | | |\n".format( 2524 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2525 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2526 ), 2527 "| | Sell: {:<21} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2530 ), 2531 ]) 2532 2533 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2534 for key in opsKeys: 2535 if key == "rub": 2536 continue 2537 2538 info.extend([ 2539 "| | | {:<28} | | |\n".format( 2540 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2541 ), 2542 "| | | {:<28} | | |\n".format( 2543 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2544 ), 2545 ]) 2546 2547 info.append(splitLine1) 2548 2549 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2550 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2551 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2552 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2553 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2554 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2555 ) 2556 2557 # --- view "Payments" lines: 2558 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2559 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2560 2561 for key in paymentsKeys: 2562 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2563 2564 info.append(splitLine1) 2565 2566 # --- view "Commissions and taxes" lines: 2567 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2568 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2569 2570 for key in comKeys: 2571 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2572 2573 info.append(splitLine1) 2574 2575 info.extend([ 2576 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2577 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2578 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2579 ]) 2580 2581 else: 2582 info.append("Broker returned no operations during this period\n") 2583 2584 # --- view "Operations" section: 2585 for item in ops: 2586 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2587 continue 2588 2589 else: 2590 self.figi = item["figi"] if item["figi"] else "" 2591 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2592 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2593 2594 # group of deals during one day: 2595 if nextDay and item["date"].split("T")[0] != nextDay: 2596 info.append(splitLine2) 2597 nextDay = "" 2598 2599 else: 2600 nextDay = item["date"].split("T")[0] # saving current day for splitting 2601 2602 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2603 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2604 self.figi if self.figi else "—", 2605 instrument["ticker"] if instrument else "—", 2606 instrument["type"] if instrument else "—", 2607 item["quantity"] if int(item["quantity"]) > 0 else "—", 2608 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2609 TKS_OPERATION_STATES[item["state"]], 2610 TKS_OPERATION_TYPES[item["operationType"]], 2611 )) 2612 2613 infoText = "".join(info) 2614 2615 if show: 2616 if self.moreDebug: 2617 uLogger.debug("Records about history of a client's operations successfully received") 2618 2619 uLogger.info(infoText) 2620 2621 if self.reportFile: 2622 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2623 fH.write(infoText) 2624 2625 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2626 2627 return ops, customStat 2628 2629 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2630 """ 2631 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2632 2633 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2634 Warning! Broker server used ISO UTC time by default. 2635 2636 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2637 Also, `historyFile` used to update history with `onlyMissing` parameter. 2638 2639 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2640 2641 :param start: see docstring in `GetDatesAsString()` method. 2642 :param end: see docstring in `GetDatesAsString()` method. 2643 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2644 `"hour"`, `"day"`. Default: `"hour"`. 2645 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2646 False by default. Warning! History appends only from last candle to current time 2647 with always update last candle! 2648 :param csvSep: separator if csv-file is used, `,` by default. 2649 :param show: if `True` then also prints Pandas DataFrame to the console. 2650 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2651 `["date", "time", "open", "high", "low", "close", "volume"]`. 2652 """ 2653 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2654 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2655 history = None # empty pandas object for history 2656 2657 if interval not in TKS_CANDLE_INTERVALS.keys(): 2658 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2659 raise Exception("Incorrect value") 2660 2661 if not (self.ticker or self.figi): 2662 uLogger.error("Ticker or FIGI must be defined!") 2663 raise Exception("Ticker or FIGI required") 2664 2665 if self.ticker and not self.figi: 2666 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2667 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2668 2669 if self.figi and not self.ticker: 2670 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2671 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2672 2673 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2674 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2675 if interval.lower() != "day": 2676 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2677 2678 delta = dtEnd - dtStart # current UTC time minus last time in file 2679 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2680 2681 # calculate history length in candles: 2682 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2683 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2684 length += 1 # to avoid fraction time 2685 2686 # calculate data blocks count: 2687 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2688 2689 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2690 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2691 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2692 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2693 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2694 2695 tempOld = None # pandas object for old history, if --only-missing key present 2696 lastTime = None # datetime object of last old candle in file 2697 2698 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2699 uLogger.debug("--only-missing key present, add only last missing candles...") 2700 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2701 2702 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2703 2704 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2705 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2706 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2707 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2708 2709 # get last datetime object from last string in file or minus 1 delta if file is empty: 2710 if len(tempOld) > 0: 2711 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2712 2713 else: 2714 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2715 2716 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2717 2718 responseJSONs = [] # raw history blocks of data 2719 2720 blockEnd = dtEnd 2721 for item in range(blocks): 2722 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2723 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2724 2725 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2726 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2727 )) 2728 2729 if blockStart == blockEnd: 2730 uLogger.debug("Skipped this zero-length block...") 2731 2732 else: 2733 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2734 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2735 self.body = str({ 2736 "figi": self.figi, 2737 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2738 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 "interval": TKS_CANDLE_INTERVALS[interval][0] 2740 }) 2741 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2742 2743 if "code" in responseJSON.keys(): 2744 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2745 2746 else: 2747 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2748 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2749 2750 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2751 2752 blockEnd = blockStart 2753 2754 printCount = len(responseJSONs) # candles to show in console 2755 if responseJSONs: 2756 tempHistory = pd.DataFrame( 2757 data={ 2758 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2759 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2761 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2762 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2763 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2764 "volume": [int(item["volume"]) for item in responseJSONs], 2765 }, 2766 index=range(len(responseJSONs)), 2767 columns=["date", "time", "open", "high", "low", "close", "volume"], 2768 ) 2769 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2770 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2771 2772 # append only newest candles to old history if --only-missing key present: 2773 if onlyMissing and tempOld is not None and lastTime is not None: 2774 index = 0 # find start index in tempHistory data: 2775 2776 for i, item in tempHistory.iterrows(): 2777 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2778 2779 if curTime == lastTime: 2780 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2781 index = i 2782 printCount = index + 1 2783 break 2784 2785 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2786 2787 else: 2788 history = tempHistory # if no `--only-missing` key then load full data from server 2789 2790 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2791 2792 if history is not None and not history.empty: 2793 if show: 2794 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2795 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2796 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2797 )) 2798 2799 else: 2800 uLogger.warning("Received an empty candles history!") 2801 2802 if self.historyFile is not None: 2803 if history is not None and not history.empty: 2804 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2805 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2806 2807 else: 2808 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2809 2810 else: 2811 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2812 2813 return history 2814 2815 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2816 """ 2817 Load candles history from csv-file and return Pandas DataFrame object. 2818 2819 See also: `History()` and `ShowHistoryChart()` methods. 2820 2821 :param filePath: path to csv-file to open. 2822 """ 2823 loadedHistory = None # init candles data object 2824 2825 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2826 2827 if os.path.exists(filePath): 2828 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2829 2830 tfStr = self.priceModel.FormattedDelta( 2831 self.priceModel.timeframe, 2832 "{days} days {hours}h {minutes}m {seconds}s", 2833 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2834 self.priceModel.timeframe, 2835 "{hours}h {minutes}m {seconds}s", 2836 ) 2837 2838 if loadedHistory is not None and not loadedHistory.empty: 2839 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2840 len(loadedHistory), 2841 tfStr, 2842 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2843 ) 2844 2845 else: 2846 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2847 2848 else: 2849 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2850 2851 return loadedHistory 2852 2853 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2854 """ 2855 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2856 2857 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2858 Default: `index.html` (both for interact and non-interact candlesticks chart). 2859 2860 See also: `History()` and `LoadHistory()` methods. 2861 2862 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2863 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2864 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2865 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2866 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2867 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2868 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2869 """ 2870 if isinstance(candles, str): 2871 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2872 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2873 2874 elif isinstance(candles, pd.DataFrame): 2875 self.priceModel.prices = candles # set candles chain from variable 2876 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2877 2878 if "datetime" not in candles.columns: 2879 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2880 2881 else: 2882 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2883 raise Exception("Incorrect value") 2884 2885 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2886 2887 if interact: 2888 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2889 2890 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2891 2892 else: 2893 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2894 2895 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2896 2897 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2898 2899 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2900 """ 2901 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2902 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2903 2904 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2905 2906 :param operation: string "Buy" or "Sell". 2907 :param lots: volume, integer count of lots >= 1. 2908 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2909 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2910 :param expDate: string "Undefined" by default or local date in future, 2911 it is a string with format `%Y-%m-%d %H:%M:%S`. 2912 :return: JSON with response from broker server. 2913 """ 2914 if self.accountId is None or not self.accountId: 2915 uLogger.error("Variable `accountId` must be defined for using this method!") 2916 raise Exception("Account ID required") 2917 2918 if operation is None or not operation or operation not in ("Buy", "Sell"): 2919 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2920 raise Exception("Incorrect value") 2921 2922 if lots is None or lots < 1: 2923 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2924 lots = 1 2925 2926 if tp is None or tp < 0: 2927 tp = 0 2928 2929 if sl is None or sl < 0: 2930 sl = 0 2931 2932 if expDate is None or not expDate: 2933 expDate = "Undefined" 2934 2935 if not (self.ticker or self.figi): 2936 uLogger.error("Ticker or FIGI must be defined!") 2937 raise Exception("Ticker or FIGI required") 2938 2939 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2940 self.ticker = instrument["ticker"] 2941 self.figi = instrument["figi"] 2942 2943 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2944 2945 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2946 self.body = str({ 2947 "figi": self.figi, 2948 "quantity": str(lots), 2949 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2950 "accountId": str(self.accountId), 2951 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2952 }) 2953 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2954 2955 if "orderId" in response.keys(): 2956 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2957 operation, response["orderId"], 2958 self.ticker, self.figi, lots, 2959 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2960 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2961 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2962 )) 2963 2964 else: 2965 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2966 2967 if tp > 0: 2968 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2969 2970 if sl > 0: 2971 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2972 2973 return response 2974 2975 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2976 """ 2977 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2978 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2979 2980 See also: `Order()` and `Trade()` docstrings. 2981 2982 :param lots: volume, integer count of lots >= 1. 2983 :param tp: float > 0, take profit price of stop-order. 2984 :param sl: float > 0, stop loss price of stop-order. 2985 :param expDate: it's a local date in future. 2986 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2987 :return: JSON with response from broker server. 2988 """ 2989 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2990 2991 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2992 """ 2993 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2994 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2995 2996 See also: `Order()` and `Trade()` docstrings. 2997 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, take profit price of stop-order. 3000 :param sl: float > 0, stop loss price of stop-order. 3001 :param expDate: it's a local date in the future. 3002 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3006 3007 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3008 """ 3009 Close position of given instruments. 3010 3011 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3012 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3013 This avoids unnecessary downloading data from the server. 3014 """ 3015 if instruments is None or not instruments: 3016 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3017 raise Exception("Ticker or FIGI required") 3018 3019 if isinstance(instruments, str): 3020 instruments = [instruments] 3021 3022 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3023 if uniqueInstruments: 3024 if portfolio is None or not portfolio: 3025 portfolio = self.Overview(show=False) 3026 3027 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3028 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3029 3030 for self.figi in uniqueInstruments: 3031 if self.figi not in allOpened: 3032 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3033 continue 3034 3035 # search open trade info about instrument by ticker: 3036 instrument = {} 3037 for iType in TKS_INSTRUMENTS: 3038 if instrument: 3039 break 3040 3041 for item in portfolio["stat"][iType]: 3042 if item["figi"] == self.figi: 3043 instrument = item 3044 break 3045 3046 if instrument: 3047 self.ticker = instrument["ticker"] 3048 self.figi = instrument["figi"] 3049 3050 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3051 self.ticker, 3052 self.figi, 3053 int(instrument["volume"]), 3054 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3055 )) 3056 3057 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3058 3059 if tradeLots > 0: 3060 if instrument["blocked"] > 0: 3061 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3062 instrument["blocked"], 3063 self.ticker, 3064 tradeLots, 3065 )) 3066 3067 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3068 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3069 3070 else: 3071 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3072 3073 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3074 """ 3075 Close all positions of given instruments with defined type. 3076 3077 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3078 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3079 This avoids unnecessary downloading data from the server. 3080 """ 3081 if iType not in TKS_INSTRUMENTS: 3082 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3083 3084 else: 3085 if portfolio is None or not portfolio: 3086 portfolio = self.Overview(show=False) 3087 3088 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3089 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3090 3091 if tickers and portfolio: 3092 self.CloseTrades(tickers, portfolio) 3093 3094 else: 3095 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3096 3097 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3098 """ 3099 Universal method to create market or limit orders with all available parameters for current `accountId`. 3100 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3101 3102 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3103 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3104 3105 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3106 then broker immediately open market order as you can do simple --buy or --sell operations! 3107 3108 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3109 When current price will go up or down to target price value then broker opens a limit order. 3110 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3111 3112 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3113 3114 :param operation: string "Buy" or "Sell". 3115 :param orderType: string "Limit" or "Stop". 3116 :param lots: volume, integer count of lots >= 1. 3117 :param targetPrice: target price > 0. This is open trade price for limit order. 3118 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3119 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3120 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3121 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3122 Stop loss order always executed by market price. 3123 :param expDate: string "Undefined" by default or local date in future. 3124 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3125 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3126 A limit order has no expiration date, it lasts until the end of the trading day. 3127 :return: JSON with response from broker server. 3128 """ 3129 if self.accountId is None or not self.accountId: 3130 uLogger.error("Variable `accountId` must be defined for using this method!") 3131 raise Exception("Account ID required") 3132 3133 if operation is None or not operation or operation not in ("Buy", "Sell"): 3134 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3135 raise Exception("Incorrect value") 3136 3137 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3138 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3139 raise Exception("Incorrect value") 3140 3141 if lots is None or lots < 1: 3142 uLogger.error("You must define trade volume > 0: integer count of lots!") 3143 raise Exception("Incorrect value") 3144 3145 if targetPrice is None or targetPrice <= 0: 3146 uLogger.error("Target price for limit-order must be greater than 0!") 3147 raise Exception("Incorrect value") 3148 3149 if limitPrice is None or limitPrice <= 0: 3150 limitPrice = targetPrice 3151 3152 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3153 stopType = "Limit" 3154 3155 if expDate is None or not expDate: 3156 expDate = "Undefined" 3157 3158 if not (self.ticker or self.figi): 3159 uLogger.error("Tocker or FIGI must be defined!") 3160 raise Exception("Ticker or FIGI required") 3161 3162 response = {} 3163 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3164 self.ticker = instrument["ticker"] 3165 self.figi = instrument["figi"] 3166 3167 if orderType == "Limit": 3168 uLogger.debug( 3169 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3170 self.ticker, self.figi, 3171 operation, lots, targetPrice, instrument["currency"], 3172 )) 3173 3174 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3175 self.body = str({ 3176 "figi": self.figi, 3177 "quantity": str(lots), 3178 "price": FloatToNano(targetPrice), 3179 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3180 "accountId": str(self.accountId), 3181 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3182 }) 3183 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3184 3185 if "orderId" in response.keys(): 3186 uLogger.info( 3187 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3188 response["orderId"], 3189 self.ticker, self.figi, 3190 operation, lots, targetPrice, instrument["currency"], 3191 )) 3192 3193 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3194 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3195 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3196 targetPrice, instrument["currency"], 3197 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3198 )) 3199 3200 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3201 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3202 targetPrice, instrument["currency"], 3203 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3204 )) 3205 3206 else: 3207 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3208 3209 if orderType == "Stop": 3210 uLogger.debug( 3211 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3212 self.ticker, self.figi, 3213 operation, lots, 3214 targetPrice, instrument["currency"], 3215 limitPrice, instrument["currency"], 3216 stopType, expDate, 3217 )) 3218 3219 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3220 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3221 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3222 3223 body = { 3224 "figi": self.figi, 3225 "quantity": str(lots), 3226 "price": FloatToNano(limitPrice), 3227 "stopPrice": FloatToNano(targetPrice), 3228 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3229 "accountId": str(self.accountId), 3230 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3231 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3232 } 3233 3234 if expDateUTC: 3235 body["expireDate"] = expDateUTC 3236 3237 self.body = str(body) 3238 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3239 3240 if "stopOrderId" in response.keys(): 3241 uLogger.info( 3242 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3243 response["stopOrderId"], 3244 self.ticker, self.figi, 3245 operation, lots, 3246 targetPrice, instrument["currency"], 3247 limitPrice, instrument["currency"], 3248 TKS_STOP_ORDER_TYPES[stopOrderType], 3249 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3250 )) 3251 3252 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3253 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3254 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3255 targetPrice, instrument["currency"], 3256 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3257 )) 3258 3259 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3260 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3261 targetPrice, instrument["currency"], 3262 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3263 )) 3264 3265 else: 3266 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3267 3268 return response 3269 3270 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3271 """ 3272 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3273 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3274 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3275 See also: `Order()` docstring. 3276 3277 :param lots: volume, integer count of lots >= 1. 3278 :param targetPrice: target price > 0. This is open trade price for limit order. 3279 :return: JSON with response from broker server. 3280 """ 3281 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3282 3283 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3284 """ 3285 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3286 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3287 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3288 target price value then broker opens a limit order. See also: `Order()` docstring. 3289 3290 :param lots: volume, integer count of lots >= 1. 3291 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3292 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3293 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3294 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3295 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3296 :param expDate: string "Undefined" by default or local date in future. 3297 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3298 This date is converting to UTC format for server. 3299 :return: JSON with response from broker server. 3300 """ 3301 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3302 3303 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3304 """ 3305 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3306 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3307 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3308 See also: `Order()` docstring. 3309 3310 :param lots: volume, integer count of lots >= 1. 3311 :param targetPrice: target price > 0. This is open trade price for limit order. 3312 :return: JSON with response from broker server. 3313 """ 3314 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3315 3316 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3317 """ 3318 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3319 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3320 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3321 target price value then broker opens a limit order. See also: `Order()` docstring. 3322 3323 :param lots: volume, integer count of lots >= 1. 3324 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3325 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3326 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3327 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3328 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3329 :param expDate: string "Undefined" by default or local date in future. 3330 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3331 This date is converting to UTC format for server. 3332 :return: JSON with response from broker server. 3333 """ 3334 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3335 3336 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3337 """ 3338 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3339 3340 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3341 :param allOrdersIDs: pre-received lists of all active pending orders. 3342 This avoids unnecessary downloading data from the server. 3343 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3344 """ 3345 if self.accountId is None or not self.accountId: 3346 uLogger.error("Variable `accountId` must be defined for using this method!") 3347 raise Exception("Account ID required") 3348 3349 if orderIDs: 3350 if allOrdersIDs is None or not allOrdersIDs: 3351 rawOrders = self.RequestPendingOrders() 3352 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3353 3354 if allStopOrdersIDs is None or not allStopOrdersIDs: 3355 rawStopOrders = self.RequestStopOrders() 3356 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3357 3358 for orderID in orderIDs: 3359 idInPendingOrders = orderID in allOrdersIDs 3360 idInStopOrders = orderID in allStopOrdersIDs 3361 3362 if not (idInPendingOrders or idInStopOrders): 3363 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3364 continue 3365 3366 else: 3367 if idInPendingOrders: 3368 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3369 3370 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3371 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3372 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3373 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3374 3375 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3376 if self.moreDebug: 3377 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3378 3379 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3380 3381 else: 3382 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3383 3384 elif idInStopOrders: 3385 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3386 3387 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3388 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3389 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3390 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3391 3392 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3393 if self.moreDebug: 3394 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3395 3396 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3397 3398 else: 3399 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3400 3401 else: 3402 continue 3403 3404 def CloseAllOrders(self) -> None: 3405 """ 3406 Gets a list of open pending and stop orders and cancel it all. 3407 """ 3408 rawOrders = self.RequestPendingOrders() 3409 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3410 lenOrders = len(allOrdersIDs) 3411 3412 rawStopOrders = self.RequestStopOrders() 3413 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3414 lenSOrders = len(allStopOrdersIDs) 3415 3416 if lenOrders > 0 or lenSOrders > 0: 3417 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3418 3419 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3420 3421 else: 3422 uLogger.info("Orders not found, nothing to cancel.") 3423 3424 def CloseAll(self, *args) -> None: 3425 """ 3426 Close all available (not blocked) opened trades and orders. 3427 3428 Also, you can select one or more keywords case-insensitive: 3429 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3430 3431 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3432 """ 3433 overview = self.Overview(show=False) # get all open trades info 3434 3435 if len(args) == 0: 3436 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3437 self.CloseAllOrders() # close all pending and stop orders 3438 3439 for iType in TKS_INSTRUMENTS: 3440 if iType != "Currencies": 3441 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3442 3443 else: 3444 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3445 lowerArgs = [x.lower() for x in args] 3446 3447 if "orders" in lowerArgs: 3448 self.CloseAllOrders() # close all pending and stop orders 3449 3450 for iType in TKS_INSTRUMENTS: 3451 if iType.lower() in lowerArgs and iType != "Currencies": 3452 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3453 3454 @staticmethod 3455 def ParseOrderParameters(operation, **inputParameters): 3456 """ 3457 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3458 3459 :param operation: string "Buy" or "Sell". 3460 :param inputParameters: this is dict of strings that looks like this 3461 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3462 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3463 "prices" key: one or more prices to open limit-orders 3464 Counts of values in lots and prices lists must be equals! 3465 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3466 """ 3467 # TODO: update order grid work with api v2 3468 pass 3469 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3470 # 3471 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3472 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3473 # raise Exception("Incorrect value") 3474 # 3475 # if "l" in inputParameters.keys(): 3476 # inputParameters["lots"] = inputParameters.pop("l") 3477 # 3478 # if "p" in inputParameters.keys(): 3479 # inputParameters["prices"] = inputParameters.pop("p") 3480 # 3481 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3482 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3483 # raise Exception("Incorrect value") 3484 # 3485 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3486 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3487 # 3488 # if len(lots) != len(prices): 3489 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3490 # raise Exception("Incorrect value") 3491 # 3492 # uLogger.debug("Extracted parameters for orders:") 3493 # uLogger.debug("lots = {}".format(lots)) 3494 # uLogger.debug("prices = {}".format(prices)) 3495 # 3496 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3497 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3498 # uLogger.debug("Order parameters: {}".format(result)) 3499 # 3500 # return result 3501 3502 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3503 """ 3504 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3505 3506 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3507 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3508 """ 3509 result = False 3510 msg = "Instrument not defined!" 3511 3512 if portfolio is None or not portfolio: 3513 portfolio = self.Overview(show=False) 3514 3515 if self.ticker: 3516 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3517 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3518 3519 for iType in TKS_INSTRUMENTS: 3520 for instrument in portfolio["stat"][iType]: 3521 if instrument["ticker"] == self.ticker: 3522 result = True 3523 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3524 break 3525 3526 elif self.figi: 3527 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3528 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3529 3530 for iType in TKS_INSTRUMENTS: 3531 for instrument in portfolio["stat"][iType]: 3532 if instrument["figi"] == self.figi: 3533 result = True 3534 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3535 break 3536 3537 else: 3538 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3539 3540 uLogger.debug(msg) 3541 3542 return result 3543 3544 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3545 """ 3546 Returns instrument is in the user's portfolio if it presents there. 3547 Instrument must be defined by `ticker` (highly priority) or `figi`. 3548 3549 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3550 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3551 """ 3552 result = None 3553 msg = "Instrument not defined!" 3554 3555 if portfolio is None or not portfolio: 3556 portfolio = self.Overview(show=False) 3557 3558 if self.ticker: 3559 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3560 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3561 3562 for iType in TKS_INSTRUMENTS: 3563 for instrument in portfolio["stat"][iType]: 3564 if instrument["ticker"] == self.ticker: 3565 result = instrument 3566 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3567 break 3568 3569 elif self.figi: 3570 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3571 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3572 3573 for iType in TKS_INSTRUMENTS: 3574 for instrument in portfolio["stat"][iType]: 3575 if instrument["figi"] == self.figi: 3576 result = instrument 3577 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3578 break 3579 3580 else: 3581 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3582 3583 uLogger.debug(msg) 3584 3585 return result 3586 3587 def RequestLimits(self) -> dict: 3588 """ 3589 Method for obtaining the available funds for withdrawal for current `accountId`. 3590 3591 See also: 3592 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3593 - `OverviewLimits()` method 3594 3595 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3596 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3597 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3598 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3599 """ 3600 if self.accountId is None or not self.accountId: 3601 uLogger.error("Variable `accountId` must be defined for using this method!") 3602 raise Exception("Account ID required") 3603 3604 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3605 3606 self.body = str({"accountId": self.accountId}) 3607 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3608 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3609 3610 if self.moreDebug: 3611 uLogger.debug("Records about available funds for withdrawal successfully received") 3612 3613 return rawLimits 3614 3615 def OverviewLimits(self, show: bool = False) -> dict: 3616 """ 3617 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3618 3619 See also: `RequestLimits()`. 3620 3621 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3622 :return: dict with raw parsed data from server and some calculated statistics about it. 3623 """ 3624 if self.accountId is None or not self.accountId: 3625 uLogger.error("Variable `accountId` must be defined for using this method!") 3626 raise Exception("Account ID required") 3627 3628 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3629 3630 view = { 3631 "rawLimits": rawLimits, 3632 "limits": { # parsed data for every currency: 3633 "money": { # this is an array of portfolio currency positions 3634 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3635 }, 3636 "blocked": { # this is an array of blocked currency 3637 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3638 }, 3639 "blockedGuarantee": { # this is locked money under collateral for futures 3640 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3641 }, 3642 }, 3643 } 3644 3645 # --- Prepare text table with limits in human-readable format: 3646 if show: 3647 info = [ 3648 "# Withdrawal limits\n\n", 3649 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3650 "* **Account ID:** [{}]\n".format(self.accountId), 3651 ] 3652 3653 if view["limits"]["money"]: 3654 info.extend([ 3655 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3656 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3657 ]) 3658 3659 else: 3660 info.append("\nNo withdrawal limits\n") 3661 3662 for curr in view["limits"]["money"].keys(): 3663 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3664 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3665 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3666 3667 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3668 "[{}]".format(curr), 3669 "{:.2f}".format(view["limits"]["money"][curr]), 3670 "{:.2f}".format(availableMoney), 3671 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3672 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3673 ) 3674 3675 if curr == "rub": 3676 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3677 3678 else: 3679 info.append(infoStr) 3680 3681 infoText = "".join(info) 3682 3683 uLogger.info(infoText) 3684 3685 if self.withdrawalLimitsFile: 3686 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3687 fH.write(infoText) 3688 3689 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3690 3691 return view 3692 3693 def RequestAccounts(self) -> dict: 3694 """ 3695 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3696 3697 See also: 3698 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3699 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3700 - `OverviewUserInfo()` method 3701 3702 :return: dict with raw data from server that contains accounts info. Example of dict: 3703 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3704 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3705 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3706 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3707 """ 3708 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3709 3710 self.body = str({}) 3711 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3712 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3713 3714 if self.moreDebug: 3715 uLogger.debug("Records about available accounts successfully received") 3716 3717 return rawAccounts 3718 3719 def RequestUserInfo(self) -> dict: 3720 """ 3721 Method for requesting common user's information. 3722 3723 See also: 3724 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3725 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3726 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3727 - `OverviewUserInfo()` method 3728 3729 :return: dict with raw data from server that contains user's information. Example of dict: 3730 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3731 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3732 """ 3733 uLogger.debug("Requesting common user's information. Wait, please...") 3734 3735 self.body = str({}) 3736 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3737 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3738 3739 if self.moreDebug: 3740 uLogger.debug("Records about current user successfully received") 3741 3742 return rawUserInfo 3743 3744 def RequestMarginStatus(self, accountId: str = None) -> dict: 3745 """ 3746 Method for requesting margin calculation for defined account ID. 3747 3748 See also: 3749 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3750 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3751 - `OverviewUserInfo()` method 3752 3753 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3754 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3755 Example of responses: 3756 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3757 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3758 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3759 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3760 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3761 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3762 """ 3763 if accountId is None or not accountId: 3764 if self.accountId is None or not self.accountId: 3765 uLogger.error("Variable `accountId` must be defined for using this method!") 3766 raise Exception("Account ID required") 3767 3768 else: 3769 accountId = self.accountId # use `self.accountId` (main ID) by default 3770 3771 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3772 3773 self.body = str({"accountId": accountId}) 3774 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3775 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3776 3777 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3778 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3779 rawMargin = {} 3780 3781 else: 3782 if self.moreDebug: 3783 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3784 3785 return rawMargin 3786 3787 def RequestTariffLimits(self) -> dict: 3788 """ 3789 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3790 3791 See also: 3792 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3793 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3794 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3795 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3796 - `OverviewUserInfo()` method 3797 3798 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3799 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3800 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3801 """ 3802 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3803 3804 self.body = str({}) 3805 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3806 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3807 3808 if self.moreDebug: 3809 uLogger.debug("Records with limits of current tariff successfully received") 3810 3811 return rawTariffLimits 3812 3813 def RequestBondCoupons(self, iJSON: dict) -> dict: 3814 """ 3815 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3816 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3817 All dates are in UTC timezone. 3818 3819 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3820 Documentation: 3821 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3822 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3823 3824 See also: `ExtendBondsData()`. 3825 3826 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3827 If raw iJSON is not data of bond then server returns an error [400] with message: 3828 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3829 :return: dictionary with bond payment calendar. Response example 3830 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3831 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3832 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3833 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3834 """ 3835 if iJSON["figi"] is None or not iJSON["figi"]: 3836 uLogger.error("FIGI must be defined for using this method!") 3837 raise Exception("FIGI required") 3838 3839 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3840 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3841 3842 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3843 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3844 self.figi, 3845 startDate, 3846 endDate, 3847 )) 3848 3849 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3850 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3851 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3852 3853 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3854 uLogger.warning("Instrument type is not bond!") 3855 3856 else: 3857 if self.moreDebug: 3858 uLogger.debug("Records about bond payment calendar successfully received") 3859 3860 return calendar 3861 3862 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3863 """ 3864 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3865 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3866 coupon yields, current yields and some statistics etc. 3867 3868 WARNING! This is too long operation if a lot of bonds requested from broker server. 3869 3870 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3871 3872 :param instruments: list of strings with tickers or FIGIs. 3873 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3874 for further used by data scientists or stock analytics. 3875 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3876 In XLSX-file and Pandas DataFrame fields mean: 3877 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3878 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3879 """ 3880 if instruments is None or not instruments: 3881 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3882 raise Exception("Ticker or FIGI required") 3883 3884 if isinstance(instruments, str): 3885 instruments = [instruments] 3886 3887 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3888 3889 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3890 3891 iCount = len(uniqueInstruments) 3892 tooLong = iCount >= 20 3893 if tooLong: 3894 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3895 3896 bonds = None 3897 for i, self.figi in enumerate(uniqueInstruments): 3898 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3899 3900 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3901 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3902 rawBond = self.SearchByFIGI(requestPrice=True) 3903 3904 # Widen raw data with UTC current time (iData["actualDateTime"]): 3905 actualDate = datetime.now(tzutc()) 3906 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3907 3908 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3909 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3910 3911 # Replace some values with human-readable: 3912 iData["nominalCurrency"] = iData["nominal"]["currency"] 3913 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3914 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3915 iData["aciCurrency"] = iData["aciValue"]["currency"] 3916 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3917 iData["issueSize"] = int(iData["issueSize"]) 3918 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3919 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3920 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3921 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3922 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3923 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3924 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3925 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3926 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3927 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3928 3929 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3930 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3931 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3932 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3933 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3934 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3935 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3936 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3937 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3938 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3939 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3940 3941 # Widen raw data with calendar data from `rawCalendar` values: 3942 calendarData = [] 3943 if "events" in iData["rawCalendar"].keys(): 3944 for item in iData["rawCalendar"]["events"]: 3945 calendarData.append({ 3946 "couponDate": item["couponDate"], 3947 "couponNumber": int(item["couponNumber"]), 3948 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3949 "payCurrency": item["payOneBond"]["currency"], 3950 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3951 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3952 "couponStartDate": item["couponStartDate"], 3953 "couponEndDate": item["couponEndDate"], 3954 "couponPeriod": item["couponPeriod"], 3955 }) 3956 3957 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3958 if "maturityDate" not in iData.keys(): 3959 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3960 3961 # Widen raw data with Coupon Rate. 3962 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3963 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3964 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3965 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3966 3967 # Widen raw data with Yield to Maturity (YTM) on current date. 3968 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3969 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3970 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3971 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3972 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3973 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3974 3975 iData["calendar"] = calendarData # adds calendar at the end 3976 3977 # Remove not used data: 3978 iData.pop("uid") 3979 iData.pop("positionUid") 3980 iData.pop("currentPrice") 3981 iData.pop("rawCalendar") 3982 3983 colNames = list(iData.keys()) 3984 if bonds is None: 3985 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3986 3987 else: 3988 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3989 3990 else: 3991 uLogger.warning("Instrument is not a bond!") 3992 3993 processed = round(100 * (i + 1) / iCount, 1) 3994 if tooLong and processed % 5 == 0: 3995 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3996 3997 else: 3998 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3999 4000 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4001 4002 # Saving bonds from Pandas DataFrame to XLSX sheet: 4003 if xlsx and self.bondsXLSXFile: 4004 with pd.ExcelWriter( 4005 path=self.bondsXLSXFile, 4006 date_format=TKS_DATE_FORMAT, 4007 datetime_format=TKS_DATE_TIME_FORMAT, 4008 mode="w", 4009 ) as writer: 4010 bonds.to_excel( 4011 writer, 4012 sheet_name="Extended bonds data", 4013 index=True, 4014 encoding="UTF-8", 4015 freeze_panes=(1, 1), 4016 ) # saving as XLSX-file with freeze first row and column as headers 4017 4018 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4019 4020 return bonds 4021 4022 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4023 """ 4024 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4025 4026 WARNING! This is too long operation if a lot of bonds requested from broker server. 4027 4028 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4029 4030 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4031 extended information about bonds: main info, current prices, bond payment calendar, 4032 coupon yields, current yields and some statistics etc. 4033 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4034 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4035 for further used by data scientists or stock analytics. 4036 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4037 """ 4038 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4039 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4040 4041 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4042 4043 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4044 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4045 calendar = None 4046 for bond in extBonds.iterrows(): 4047 for item in bond[1]["calendar"]: 4048 cData = { 4049 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4050 "couponDate": item["couponDate"], 4051 "figi": bond[1]["figi"], 4052 "ticker": bond[1]["ticker"], 4053 "name": bond[1]["name"], 4054 "couponNumber": item["couponNumber"], 4055 "payOneBond": item["payOneBond"], 4056 "payCurrency": item["payCurrency"], 4057 "couponType": item["couponType"], 4058 "couponPeriod": item["couponPeriod"], 4059 "fixDate": item["fixDate"], 4060 "couponStartDate": item["couponStartDate"], 4061 "couponEndDate": item["couponEndDate"], 4062 } 4063 4064 if calendar is None: 4065 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4066 4067 else: 4068 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4069 4070 if calendar is not None: 4071 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4072 4073 # Saving calendar from Pandas DataFrame to XLSX sheet: 4074 if xlsx: 4075 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4076 4077 with pd.ExcelWriter( 4078 path=xlsxCalendarFile, 4079 date_format=TKS_DATE_FORMAT, 4080 datetime_format=TKS_DATE_TIME_FORMAT, 4081 mode="w", 4082 ) as writer: 4083 humanReadable = calendar.copy(deep=True) 4084 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4085 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4086 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4087 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4088 humanReadable.columns = colNames # human-readable column names 4089 4090 humanReadable.to_excel( 4091 writer, 4092 sheet_name="Bond payments calendar", 4093 index=False, 4094 encoding="UTF-8", 4095 freeze_panes=(1, 2), 4096 ) # saving as XLSX-file with freeze first row and column as headers 4097 4098 del humanReadable # release df in memory 4099 4100 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4101 4102 return calendar 4103 4104 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4105 """ 4106 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4107 Also, creates Markdown file with calendar data, `calendar.md` by default. 4108 4109 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4110 4111 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4112 extended information about bonds: main info, current prices, bond payment calendar, 4113 coupon yields, current yields and some statistics etc. 4114 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4115 :param show: if `True` then also printing bonds payment calendar to the console, 4116 otherwise save to file `calendarFile` only. `False` by default. 4117 :return: multilines text in Markdown format with bonds payment calendar as a table. 4118 """ 4119 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4120 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4121 4122 infoText = "# Bond payments calendar\n\n" 4123 4124 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4125 4126 if not (calendar is None or calendar.empty): 4127 splitLine = "| | | | | | | | | |\n" 4128 4129 info = [ 4130 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4131 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4132 ] 4133 4134 newMonth = False 4135 notOneBond = calendar["figi"].nunique() > 1 4136 for i, bond in enumerate(calendar.iterrows()): 4137 if newMonth and notOneBond: 4138 info.append(splitLine) 4139 4140 info.append( 4141 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4142 " √" if bond[1]["paid"] else " —", 4143 bond[1]["couponDate"].split("T")[0], 4144 bond[1]["figi"], 4145 bond[1]["ticker"], 4146 bond[1]["couponNumber"], 4147 "{} {}".format( 4148 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4149 bond[1]["payCurrency"], 4150 ), 4151 bond[1]["couponType"], 4152 bond[1]["couponPeriod"], 4153 bond[1]["fixDate"].split("T")[0], 4154 ) 4155 ) 4156 4157 if i < len(calendar.values) - 1: 4158 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4159 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4160 newMonth = False if curDate.month == nextDate.month else True 4161 4162 else: 4163 newMonth = False 4164 4165 infoText += "".join(info) 4166 4167 if show: 4168 uLogger.info("{}".format(infoText)) 4169 4170 if self.calendarFile is not None: 4171 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4172 fH.write(infoText) 4173 4174 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4175 4176 else: 4177 infoText += "No data\n" 4178 4179 return infoText 4180 4181 def OverviewAccounts(self, show: bool = False) -> dict: 4182 """ 4183 Method for parsing and show simple table with all available user accounts. 4184 4185 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4186 4187 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4188 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4189 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4190 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4191 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4192 "closed": "—", "access": "Full access" }, ...}}` 4193 """ 4194 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4195 4196 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4197 accounts = { 4198 item["id"]: { 4199 "type": TKS_ACCOUNT_TYPES[item["type"]], 4200 "name": item["name"], 4201 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4202 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4203 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4204 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4205 } for item in rawAccounts["accounts"] 4206 } 4207 4208 # Raw and parsed data with some fields replaced in "stat" section: 4209 view = { 4210 "rawAccounts": rawAccounts, 4211 "stat": accounts, 4212 } 4213 4214 # --- Prepare simple text table with only accounts data in human-readable format: 4215 if show: 4216 info = [ 4217 "# User accounts\n\n", 4218 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4219 "| Account ID | Type | Status | Name |\n", 4220 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4221 ] 4222 4223 for account in view["stat"].keys(): 4224 info.extend([ 4225 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4226 account, 4227 view["stat"][account]["type"], 4228 view["stat"][account]["status"], 4229 view["stat"][account]["name"], 4230 ) 4231 ]) 4232 4233 infoText = "".join(info) 4234 4235 uLogger.info(infoText) 4236 4237 if self.userAccountsFile: 4238 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4239 fH.write(infoText) 4240 4241 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4242 4243 return view 4244 4245 def OverviewUserInfo(self, show: bool = False) -> dict: 4246 """ 4247 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4248 4249 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4250 4251 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4252 :return: dict with raw parsed data from server and some calculated statistics about it. 4253 """ 4254 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4255 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4256 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4257 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4258 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4259 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4260 4261 # This is dict with parsed common user data: 4262 userInfo = { 4263 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4264 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4265 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4266 "tariff": rawUserInfo["tariff"], 4267 } 4268 4269 # This is an array of dict with parsed margin statuses for every account IDs: 4270 margins = {} 4271 for accountId in accounts.keys(): 4272 if rawMargins[accountId]: 4273 margins[accountId] = { 4274 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4275 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4276 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4277 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4278 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4279 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4280 } 4281 4282 else: 4283 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4284 4285 unary = {} # unary-connection limits 4286 for item in rawTariffLimits["unaryLimits"]: 4287 if item["limitPerMinute"] in unary.keys(): 4288 unary[item["limitPerMinute"]].extend(item["methods"]) 4289 4290 else: 4291 unary[item["limitPerMinute"]] = item["methods"] 4292 4293 stream = {} # stream-connection limits 4294 for item in rawTariffLimits["streamLimits"]: 4295 if item["limit"] in stream.keys(): 4296 stream[item["limit"]].extend(item["streams"]) 4297 4298 else: 4299 stream[item["limit"]] = item["streams"] 4300 4301 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4302 limits = { 4303 "unary": unary, 4304 "stream": stream, 4305 } 4306 4307 # Raw and parsed data as an output result: 4308 view = { 4309 "rawUserInfo": rawUserInfo, 4310 "rawAccounts": rawAccounts, 4311 "rawMargins": rawMargins, 4312 "rawTariffLimits": rawTariffLimits, 4313 "stat": { 4314 "userInfo": userInfo, 4315 "accounts": accounts, 4316 "margins": margins, 4317 "limits": limits, 4318 }, 4319 } 4320 4321 # --- Prepare text table with user information in human-readable format: 4322 if show: 4323 info = [ 4324 "# Full user information\n\n", 4325 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4326 "## Common information\n\n", 4327 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4328 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4329 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4330 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4331 "\n## User accounts\n\n", 4332 ] 4333 4334 for account in view["stat"]["accounts"].keys(): 4335 info.extend([ 4336 "### ID: [{}]\n\n".format(account), 4337 "| Parameters | Values |\n", 4338 "|----------------------|--------------------------------------------------------------|\n", 4339 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4340 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4341 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4342 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4343 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4344 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4345 ]) 4346 4347 if margins[account]: 4348 info.extend([ 4349 "| Margin status: | Enabled |\n", 4350 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4351 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4352 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4353 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4354 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4355 ]) 4356 4357 else: 4358 info.append("| Margin status: | Disabled |\n\n") 4359 4360 info.extend([ 4361 "\n## Current user tariff limits\n", 4362 "\nSee also:\n", 4363 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4364 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4365 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4366 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4367 "\n### Unary limits\n", 4368 ]) 4369 4370 if unary: 4371 for key, values in sorted(unary.items()): 4372 info.append("\n* Max requests per minute: {}\n".format(key)) 4373 4374 for value in values: 4375 info.append(" - {}\n".format(value)) 4376 4377 else: 4378 info.append("\nNot available\n") 4379 4380 info.append("\n### Stream limits\n") 4381 4382 if stream: 4383 for key, values in sorted(stream.items()): 4384 info.append("\n* Max stream connections: {}\n".format(key)) 4385 4386 for value in values: 4387 info.append(" - {}\n".format(value)) 4388 4389 else: 4390 info.append("\nNot available\n") 4391 4392 infoText = "".join(info) 4393 4394 uLogger.info(infoText) 4395 4396 if self.userInfoFile: 4397 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4398 fH.write(infoText) 4399 4400 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4401 4402 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.moreDebug = False 301 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 302 303 self.historyFile = None 304 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 305 306 See also: `History()`. 307 """ 308 309 self.htmlHistoryFile = "index.html" 310 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 311 312 See also: `ShowHistoryChart()`. 313 """ 314 315 self.instrumentsFile = "instruments.md" 316 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 317 318 See also: `ShowInstrumentsInfo()`. 319 """ 320 321 self.searchResultsFile = "search-results.md" 322 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 323 324 See also: `SearchInstruments()`. 325 """ 326 327 self.pricesFile = "prices.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `GetListOfPrices()`. 331 """ 332 333 self.infoFile = "info.md" 334 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 335 336 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 337 """ 338 339 self.bondsXLSXFile = "ext-bonds.xlsx" 340 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 341 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 342 343 See also: `ExtendBondsData()`. 344 """ 345 346 self.calendarFile = "calendar.md" 347 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 348 349 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 350 351 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 352 """ 353 354 self.overviewFile = "overview.md" 355 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 356 357 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 358 """ 359 360 self.overviewDigestFile = "overview-digest.md" 361 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 362 363 See also: `Overview()` with parameter `details="digest"`. 364 """ 365 366 self.overviewPositionsFile = "overview-positions.md" 367 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 368 369 See also: `Overview()` with parameter `details="positions"`. 370 """ 371 372 self.overviewOrdersFile = "overview-orders.md" 373 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 374 375 See also: `Overview()` with parameter `details="orders"`. 376 """ 377 378 self.overviewAnalyticsFile = "overview-analytics.md" 379 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 380 381 See also: `Overview()` with parameter `details="analytics"`. 382 """ 383 384 self.reportFile = "deals.md" 385 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 386 387 See also: `Deals()`. 388 """ 389 390 self.withdrawalLimitsFile = "limits.md" 391 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 392 393 See also: `OverviewLimits()` and `RequestLimits()`. 394 """ 395 396 self.userInfoFile = "user-info.md" 397 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 398 399 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 400 """ 401 402 self.userAccountsFile = "accounts.md" 403 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 404 405 See also: `OverviewAccounts()`, `RequestAccounts()`. 406 """ 407 408 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 409 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 410 411 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 412 413 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 414 """ 415 416 self.iList = None # init iList for raw instruments data 417 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 418 419 See also: `Listing()`, `DumpInstruments()`. 420 """ 421 422 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 423 if useCache: 424 if os.path.exists(self.iListDumpFile): 425 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 426 curTime = datetime.now(tzutc()) 427 428 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 429 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 430 431 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 432 433 else: 434 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 435 436 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 437 os.path.abspath(self.iListDumpFile), 438 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 439 )) 440 441 else: 442 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 443 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 444 445 else: 446 self.iList = self.Listing() # request new raw instruments data from broker server 447 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 448 449 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 450 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 451 452 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 453 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
469 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 470 """ 471 Send GET or POST request to broker server and receive JSON object. 472 473 self.header: must be defining with dictionary of headers. 474 self.body: if define then used as request body. None by default. 475 self.timeout: global request timeout, 15 seconds by default. 476 :param url: url with REST request. 477 :param reqType: send "GET" or "POST" request. "GET" by default. 478 :param retry: how many times retry after first request if an 5xx server errors occurred. 479 :param pause: sleep time in seconds between retries. 480 :return: response JSON (dictionary) from broker. 481 """ 482 if reqType not in ("GET", "POST"): 483 uLogger.error("You can define request type: 'GET' or 'POST'!") 484 raise Exception("Incorrect value") 485 486 if self.moreDebug: 487 uLogger.debug("Request parameters:") 488 uLogger.debug(" - REST API URL: {}".format(url)) 489 uLogger.debug(" - request type: {}".format(reqType)) 490 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 491 uLogger.debug(" - body:\n{}".format(self.body)) 492 493 # fast hack to avoid all operations with some tickers/FIGI 494 responseJSON = {} 495 oK = True 496 for item in self.exclude: 497 if item in url: 498 if self.moreDebug: 499 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 500 501 oK = False 502 break 503 504 if oK: 505 counter = 0 506 response = None 507 errMsg = "" 508 509 while not response and counter <= retry: 510 if reqType == "GET": 511 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 512 513 if reqType == "POST": 514 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if self.moreDebug: 517 uLogger.debug("Response:") 518 uLogger.debug(" - status code: {}".format(response.status_code)) 519 uLogger.debug(" - reason: {}".format(response.reason)) 520 uLogger.debug(" - body length: {}".format(len(response.text))) 521 uLogger.debug(" - headers:\n{}".format(response.headers)) 522 523 # Server returns some headers: 524 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 525 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 526 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 527 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 528 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 529 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 530 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 531 sleep(rateLimitWait) 532 533 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 534 if 400 <= response.status_code < 500: 535 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 536 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 537 counter = retry + 1 538 539 if 500 <= response.status_code < 600: 540 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 541 uLogger.debug(" - not oK, {}".format(errMsg)) 542 counter += 1 543 544 if counter <= retry: 545 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 546 sleep(pause) 547 548 responseJSON = self._ParseJSON(rawData=response.text) 549 550 if errMsg: 551 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 552 uLogger.error(" - not oK, {}".format(errMsg)) 553 554 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
587 def Listing(self) -> dict: 588 """ 589 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 590 591 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 592 """ 593 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 594 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 595 596 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 597 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 598 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 599 600 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 601 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 602 poolUpdater.close() 603 604 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 605 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 606 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 607 608 # calculate minimum price increment (step) for all instruments and set up instrument's type: 609 for iType in iList.keys(): 610 for ticker in iList[iType]: 611 iList[iType][ticker]["type"] = iType 612 613 if "minPriceIncrement" in iList[iType][ticker].keys(): 614 iList[iType][ticker]["step"] = NanoToFloat( 615 iList[iType][ticker]["minPriceIncrement"]["units"], 616 iList[iType][ticker]["minPriceIncrement"]["nano"], 617 ) 618 619 else: 620 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 621 622 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
624 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 625 """ 626 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 627 628 See also: `DumpInstruments()`, `Listing()`. 629 630 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 631 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 632 """ 633 if self.iListDumpFile is None or not self.iListDumpFile: 634 uLogger.error("Output name of dump file must be defined!") 635 raise Exception("Filename required") 636 637 if not self.iList or forceUpdate: 638 self.iList = self.Listing() 639 640 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 641 642 # Save as XLSX with separated sheets for every type of instruments: 643 with pd.ExcelWriter( 644 path=xlsxDumpFile, 645 date_format=TKS_DATE_FORMAT, 646 datetime_format=TKS_DATE_TIME_FORMAT, 647 mode="w", 648 ) as writer: 649 for iType in TKS_INSTRUMENTS: 650 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 651 df = df[sorted(df)] # sorted by column names 652 df = df.applymap( 653 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 654 na_action="ignore", 655 ) # converting numbers from nano-type to float in every cell 656 df.to_excel( 657 writer, 658 sheet_name=iType, 659 encoding="UTF-8", 660 freeze_panes=(1, 1), 661 ) # saving as XLSX-file with freeze first row and column as headers 662 663 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
665 def DumpInstruments(self, forceUpdate: bool = True) -> str: 666 """ 667 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 668 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 669 670 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 671 672 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 673 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 674 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 675 """ 676 if self.iListDumpFile is None or not self.iListDumpFile: 677 uLogger.error("Output name of dump file must be defined!") 678 raise Exception("Filename required") 679 680 if not self.iList or forceUpdate: 681 self.iList = self.Listing() 682 683 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 684 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 685 fH.write(jsonDump) 686 687 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 688 689 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
691 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 692 """ 693 Show information about one instrument defined by json data and prints it in Markdown format. 694 695 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 696 697 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 698 :param show: if `True` then also printing information about instrument and its current price. 699 :return: multilines text in Markdown format with information about one instrument. 700 """ 701 splitLine = "| | |\n" 702 infoText = "" 703 704 if iJSON is not None and iJSON and isinstance(iJSON, dict): 705 info = [ 706 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 707 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 708 "| Parameters | Values |\n", 709 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 710 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 711 "| Full name: | {:<54} |\n".format(iJSON["name"]), 712 ] 713 714 if "sector" in iJSON.keys() and iJSON["sector"]: 715 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 716 717 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 718 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 719 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 720 ))) 721 722 info.extend([ 723 splitLine, 724 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 725 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 726 ]) 727 728 if "isin" in iJSON.keys() and iJSON["isin"]: 729 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 730 731 if "classCode" in iJSON.keys(): 732 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 733 734 info.extend([ 735 splitLine, 736 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 737 splitLine, 738 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 739 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 740 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 741 ]) 742 743 if iJSON["figi"]: 744 self.figi = iJSON["figi"] 745 iJSON = iJSON | self.RequestTradingStatus() 746 747 info.extend([ 748 splitLine, 749 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 750 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 751 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 752 ]) 753 754 info.append(splitLine) 755 756 if "type" in iJSON.keys() and iJSON["type"]: 757 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 758 759 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 760 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 761 762 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 763 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 764 765 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 766 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 767 768 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 769 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 770 771 if "focusType" in iJSON.keys() and iJSON["focusType"]: 772 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 773 774 if "assetType" in iJSON.keys() and iJSON["assetType"]: 775 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 776 777 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 778 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 779 780 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 781 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 782 783 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 784 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 785 786 if "currency" in iJSON.keys(): 787 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 788 789 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 790 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 791 792 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 793 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 794 795 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 796 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 799 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 800 801 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 802 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 803 804 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 805 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 806 807 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 808 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 809 810 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 811 info.append("| Perpetual bond: | Yes |\n") 812 813 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 814 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 815 816 iExt = None 817 if iJSON["type"] == "Bonds": 818 info.extend([ 819 splitLine, 820 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 821 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 822 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 823 iJSON["nominal"]["currency"], 824 )), 825 ]) 826 827 if "floatingCouponFlag" in iJSON.keys(): 828 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 829 830 if "amortizationFlag" in iJSON.keys(): 831 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 832 833 info.append(splitLine) 834 835 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 836 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 837 838 if iJSON["figi"]: 839 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 840 841 info.extend([ 842 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 843 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 844 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 845 ]) 846 847 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 848 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 849 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 850 iJSON["aciValue"]["currency"] 851 ))) 852 853 if "currentPrice" in iJSON.keys(): 854 info.append(splitLine) 855 856 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 857 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 858 859 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 860 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 861 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 862 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 863 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 864 865 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 866 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 867 868 info.extend([ 869 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 870 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 871 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 872 )), 873 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 874 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 875 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 876 )), 877 "| Changes between last deal price and last close | {:<54} |\n".format( 878 "{:.2f}%{}".format( 879 iJSON["currentPrice"]["changes"], 880 " ({}{:.2f} {})".format( 881 "+" if bondChangesDelta > 0 else "", 882 bondChangesDelta, 883 aciCurrency 884 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 885 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 886 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 887 currency 888 ), 889 ) 890 ), 891 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 892 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 893 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 894 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 897 )), 898 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 899 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 903 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 904 )), 905 ]) 906 907 if "lot" in iJSON.keys(): 908 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 909 910 if "step" in iJSON.keys() and iJSON["step"] != 0: 911 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 912 913 # Add bond payment calendar: 914 if iJSON["type"] == "Bonds": 915 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 916 info.extend(["\n", strCalendar]) 917 918 infoText += "".join(info) 919 920 if show: 921 uLogger.info("{}".format(infoText)) 922 923 else: 924 uLogger.debug("{}".format(infoText)) 925 926 if self.infoFile is not None: 927 with open(self.infoFile, "w", encoding="UTF-8") as fH: 928 fH.write(infoText) 929 930 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 931 932 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
934 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 935 """ 936 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 937 938 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 939 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 940 :return: JSON formatted data with information about instrument. 941 """ 942 tickerJSON = {} 943 if self.moreDebug: 944 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 945 946 if not self.ticker: 947 uLogger.warning("self.ticker variable is not be empty!") 948 949 else: 950 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 951 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 952 raise Exception("Instrument not allowed") 953 954 if not self.iList: 955 self.iList = self.Listing() 956 957 if self.ticker in self.iList["Shares"].keys(): 958 tickerJSON = self.iList["Shares"][self.ticker] 959 if self.moreDebug: 960 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 961 962 elif self.ticker in self.iList["Currencies"].keys(): 963 tickerJSON = self.iList["Currencies"][self.ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Bonds"].keys(): 968 tickerJSON = self.iList["Bonds"][self.ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Etfs"].keys(): 973 tickerJSON = self.iList["Etfs"][self.ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Futures"].keys(): 978 tickerJSON = self.iList["Futures"][self.ticker] 979 if self.moreDebug: 980 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 981 982 if tickerJSON: 983 self.figi = tickerJSON["figi"] 984 985 if requestPrice: 986 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 987 988 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 989 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 990 991 else: 992 tickerJSON["currentPrice"]["changes"] = 0 993 994 if show: 995 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 996 997 else: 998 if show: 999 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1000 1001 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1003 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1004 """ 1005 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1006 1007 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1008 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1009 :return: JSON formatted data with information about instrument. 1010 """ 1011 figiJSON = {} 1012 if self.moreDebug: 1013 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1014 1015 if not self.figi: 1016 uLogger.warning("self.figi variable is not be empty!") 1017 1018 else: 1019 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1020 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1021 raise Exception("Instrument not allowed") 1022 1023 if not self.iList: 1024 self.iList = self.Listing() 1025 1026 for item in self.iList["Shares"].keys(): 1027 if self.figi == self.iList["Shares"][item]["figi"]: 1028 figiJSON = self.iList["Shares"][item] 1029 1030 if self.moreDebug: 1031 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1032 1033 break 1034 1035 if not figiJSON: 1036 for item in self.iList["Currencies"].keys(): 1037 if self.figi == self.iList["Currencies"][item]["figi"]: 1038 figiJSON = self.iList["Currencies"][item] 1039 1040 if self.moreDebug: 1041 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1042 1043 break 1044 1045 if not figiJSON: 1046 for item in self.iList["Bonds"].keys(): 1047 if self.figi == self.iList["Bonds"][item]["figi"]: 1048 figiJSON = self.iList["Bonds"][item] 1049 1050 if self.moreDebug: 1051 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1052 1053 break 1054 1055 if not figiJSON: 1056 for item in self.iList["Etfs"].keys(): 1057 if self.figi == self.iList["Etfs"][item]["figi"]: 1058 figiJSON = self.iList["Etfs"][item] 1059 1060 if self.moreDebug: 1061 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1062 1063 break 1064 1065 if not figiJSON: 1066 for item in self.iList["Futures"].keys(): 1067 if self.figi == self.iList["Futures"][item]["figi"]: 1068 figiJSON = self.iList["Futures"][item] 1069 1070 if self.moreDebug: 1071 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1072 1073 break 1074 1075 if figiJSON: 1076 self.figi = figiJSON["figi"] 1077 self.ticker = figiJSON["ticker"] 1078 1079 if requestPrice: 1080 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1081 1082 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1083 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1084 1085 else: 1086 figiJSON["currentPrice"]["changes"] = 0 1087 1088 if show: 1089 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1090 1091 else: 1092 if show: 1093 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1094 1095 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1097 def GetCurrentPrices(self, show: bool = True) -> dict: 1098 """ 1099 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1100 `{"buy": [{"price": 1243.8, "quantity": 193}, 1101 {"price": 1244.0, "quantity": 168}, 1102 {"price": 1244.8, "quantity": 5}, 1103 {"price": 1245.0, "quantity": 61}, 1104 {"price": 1245.4, "quantity": 60}], 1105 "sell": [{"price": 1243.6, "quantity": 8}, 1106 {"price": 1242.6, "quantity": 10}, 1107 {"price": 1242.4, "quantity": 18}, 1108 {"price": 1242.2, "quantity": 50}, 1109 {"price": 1242.0, "quantity": 113}], 1110 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1111 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1112 - sell: list of dicts with Buyers prices, 1113 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1114 - quantity: volume value by current price in lots, 1115 - limitUp: current trade session limit price, maximum, 1116 - limitDown: current trade session limit price, minimum, 1117 - lastPrice: last deal price of the instrument, 1118 - closePrice: previous trade session close price of the instrument. 1119 1120 See also: `SearchByTicker()` and `SearchByFIGI()`. 1121 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1122 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1123 1124 :param show: if `True` then print DOM to log and console. 1125 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1126 If an error occurred then returns an empty record: 1127 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1128 """ 1129 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1130 1131 if self.depth < 1: 1132 uLogger.error("Depth of Market (DOM) must be >=1!") 1133 raise Exception("Incorrect value") 1134 1135 if not (self.ticker or self.figi): 1136 uLogger.error("self.ticker or self.figi variables must be defined!") 1137 raise Exception("Ticker or FIGI required") 1138 1139 if self.ticker and not self.figi: 1140 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1141 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1142 1143 if not self.ticker and self.figi: 1144 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1145 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1146 1147 if not self.figi: 1148 uLogger.error("FIGI is not defined!") 1149 raise Exception("Ticker or FIGI required") 1150 1151 else: 1152 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1153 1154 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1155 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1156 self.body = str({"figi": self.figi, "depth": self.depth}) 1157 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1158 1159 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1160 # list of dicts with sellers orders: 1161 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1162 1163 # list of dicts with buyers orders: 1164 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1165 1166 # max price of instrument at this time: 1167 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1168 1169 # min price of instrument at this time: 1170 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1171 1172 # last price of deal with instrument: 1173 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1174 1175 # last close price of instrument: 1176 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1177 1178 else: 1179 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1180 uLogger.debug("Server response: {}".format(pricesResponse)) 1181 1182 if show: 1183 if prices["buy"] or prices["sell"]: 1184 info = [ 1185 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1186 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1187 self.ticker, 1188 self.figi, 1189 self.depth, 1190 ), 1191 "-" * 60, "\n", 1192 " Orders of Buyers | Orders of Sellers\n", 1193 "-" * 60, "\n", 1194 " Sell prices (volumes) | Buy prices (volumes)\n", 1195 "-" * 60, "\n", 1196 ] 1197 1198 if not prices["buy"]: 1199 info.append(" | No orders!\n") 1200 sumBuy = 0 1201 1202 else: 1203 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1204 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1205 for item in maxMinSorted: 1206 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1207 1208 if not prices["sell"]: 1209 info.append("No orders! |\n") 1210 sumSell = 0 1211 1212 else: 1213 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1214 for item in prices["sell"]: 1215 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1216 1217 info.extend([ 1218 "-" * 60, "\n", 1219 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1220 "-" * 60, "\n", 1221 ]) 1222 1223 infoText = "".join(info) 1224 1225 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1226 1227 else: 1228 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1229 1230 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1232 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1233 """ 1234 This method get and show information about all available broker instruments for current user account. 1235 If `instrumentsFile` string is not empty then also save information to this file. 1236 1237 :param show: if `True` then print results to console, if `False` - print only to file. 1238 :return: multi-lines string with all available broker instruments 1239 """ 1240 if not self.iList: 1241 self.iList = self.Listing() 1242 1243 info = [ 1244 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1245 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1246 ] 1247 1248 # add instruments count by type: 1249 for iType in self.iList.keys(): 1250 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1251 1252 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1253 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1254 1255 # generating info tables with all instruments by type: 1256 for iType in self.iList.keys(): 1257 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1258 1259 for instrument in self.iList[iType].keys(): 1260 iName = self.iList[iType][instrument]["name"] # instrument's name 1261 if len(iName) > 57: 1262 iName = "{}...".format(iName[:54]) # right trim for a long string 1263 1264 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1265 self.iList[iType][instrument]["ticker"], 1266 iName, 1267 self.iList[iType][instrument]["figi"], 1268 self.iList[iType][instrument]["currency"], 1269 self.iList[iType][instrument]["lot"], 1270 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1271 )) 1272 1273 infoText = "".join(info) 1274 1275 if show: 1276 uLogger.info(infoText) 1277 1278 if self.instrumentsFile: 1279 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1280 fH.write(infoText) 1281 1282 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1283 1284 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1286 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1287 """ 1288 This method search and show information about instruments by part of its ticker, FIGI or name. 1289 If `searchResultsFile` string is not empty then also save information to this file. 1290 1291 :param pattern: string with part of ticker, FIGI or instrument's name. 1292 :param show: if `True` then print results to console, if `False` - return list of result only. 1293 :return: list of dictionaries with all found instruments. 1294 """ 1295 if not self.iList: 1296 self.iList = self.Listing() 1297 1298 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1299 compiledPattern = re.compile(pattern, re.IGNORECASE) 1300 1301 for iType in self.iList: 1302 for instrument in self.iList[iType].values(): 1303 searchResult = compiledPattern.search(" ".join( 1304 [instrument["ticker"], instrument["figi"], instrument["name"]] 1305 )) 1306 1307 if searchResult: 1308 searchResults[iType][instrument["ticker"]] = instrument 1309 1310 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1311 info = [ 1312 "# Search results\n\n", 1313 "* **Search pattern:** [{}]\n".format(pattern), 1314 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1315 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1316 ] 1317 infoShort = info[:] 1318 1319 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1320 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1321 skippedLine = "| ... | ... | ... | ... |\n" 1322 1323 if resultsLen == 0: 1324 info.append("\nNo results\n") 1325 infoShort.append("\nNo results\n") 1326 uLogger.warning("No results. Try changing your search pattern.") 1327 1328 else: 1329 for iType in searchResults: 1330 iTypeValuesCount = len(searchResults[iType].values()) 1331 if iTypeValuesCount > 0: 1332 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1333 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1334 1335 for instrument in searchResults[iType].values(): 1336 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1337 instrument["type"], 1338 instrument["ticker"], 1339 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1340 instrument["figi"], 1341 )) 1342 1343 if iTypeValuesCount <= 5: 1344 infoShort.extend(info[-iTypeValuesCount:]) 1345 1346 else: 1347 infoShort.extend(info[-5:]) 1348 infoShort.append(skippedLine) 1349 1350 infoText = "".join(info) 1351 infoTextShort = "".join(infoShort) 1352 1353 if show: 1354 uLogger.info(infoTextShort) 1355 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1356 1357 if self.searchResultsFile: 1358 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1359 fH.write(infoText) 1360 1361 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1362 1363 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1365 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1366 """ 1367 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1368 1369 :param instruments: list of strings with tickers or FIGIs. 1370 :return: list with unique instrument FIGIs only. 1371 """ 1372 requestedInstruments = [] 1373 for iName in instruments: 1374 if iName not in self.aliases.keys(): 1375 if iName not in requestedInstruments: 1376 requestedInstruments.append(iName) 1377 1378 else: 1379 if iName not in requestedInstruments: 1380 if self.aliases[iName] not in requestedInstruments: 1381 requestedInstruments.append(self.aliases[iName]) 1382 1383 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1384 1385 onlyUniqueFIGIs = [] 1386 for iName in requestedInstruments: 1387 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1388 continue 1389 1390 self.ticker = iName 1391 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1392 1393 if not iData: 1394 self.ticker = "" 1395 self.figi = iName 1396 1397 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1398 1399 if not iData: 1400 self.figi = "" 1401 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1402 1403 if iData and iData["figi"] not in onlyUniqueFIGIs: 1404 onlyUniqueFIGIs.append(iData["figi"]) 1405 1406 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1407 1408 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1410 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1411 """ 1412 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1413 See limits: https://tinkoff.github.io/investAPI/limits/ 1414 If `pricesFile` string is not empty then also save information to this file. 1415 1416 :param instruments: list of strings with tickers or FIGIs. 1417 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1418 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1419 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1420 """ 1421 if instruments is None or not instruments: 1422 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1423 raise Exception("Ticker or FIGI required") 1424 1425 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1426 1427 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1428 1429 iList = [] # trying to get info and current prices about all unique instruments: 1430 for self.figi in onlyUniqueFIGIs: 1431 iData = self.SearchByFIGI(requestPrice=True) 1432 iList.append(iData) 1433 1434 self.ShowListOfPrices(iList, show) 1435 1436 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1438 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1439 """ 1440 Show table contains current prices of given instruments. 1441 1442 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1443 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1444 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1445 :return: multilines text in Markdown format as a table contains current prices. 1446 """ 1447 infoText = "" 1448 1449 if show or self.pricesFile: 1450 info = [ 1451 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1452 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1453 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1454 ] 1455 1456 for item in iList: 1457 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1458 item["ticker"], 1459 item["figi"], 1460 item["type"], 1461 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1462 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1463 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1464 "{} / {}".format( 1465 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1466 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1467 ), 1468 "{} / {}".format( 1469 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1470 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1471 ), 1472 item["currency"], 1473 )) 1474 1475 infoText = "".join(info) 1476 1477 if show: 1478 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1479 1480 if self.pricesFile: 1481 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1482 fH.write(infoText) 1483 1484 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1485 1486 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1488 def RequestTradingStatus(self) -> dict: 1489 """ 1490 Requesting trading status for the instrument defined by `figi` variable. 1491 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1492 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1493 1494 :return: dictionary with trading status attributes. Response example: 1495 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1496 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1497 """ 1498 if self.figi is None or not self.figi: 1499 uLogger.error("Variable `figi` must be defined for using this method!") 1500 raise Exception("FIGI required") 1501 1502 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1503 1504 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1505 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1506 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1507 1508 if self.moreDebug: 1509 uLogger.debug("Records about current trading status successfully received") 1510 1511 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1513 def RequestPortfolio(self) -> dict: 1514 """ 1515 Requesting actual user's portfolio for current `accountId`. 1516 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1517 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1518 1519 :return: dictionary with user's portfolio. 1520 """ 1521 if self.accountId is None or not self.accountId: 1522 uLogger.error("Variable `accountId` must be defined for using this method!") 1523 raise Exception("Account ID required") 1524 1525 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1526 1527 self.body = str({"accountId": self.accountId}) 1528 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1529 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1530 1531 if self.moreDebug: 1532 uLogger.debug("Records about user's portfolio successfully received") 1533 1534 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1536 def RequestPositions(self) -> dict: 1537 """ 1538 Requesting open positions by currencies and instruments for current `accountId`. 1539 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1540 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1541 1542 :return: dictionary with open positions by instruments. 1543 """ 1544 if self.accountId is None or not self.accountId: 1545 uLogger.error("Variable `accountId` must be defined for using this method!") 1546 raise Exception("Account ID required") 1547 1548 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1549 1550 self.body = str({"accountId": self.accountId}) 1551 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1552 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1553 1554 if self.moreDebug: 1555 uLogger.debug("Records about current open positions successfully received") 1556 1557 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1559 def RequestPendingOrders(self) -> list: 1560 """ 1561 Requesting current actual pending orders for current `accountId`. 1562 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1563 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1564 1565 :return: list of dictionaries with pending orders. 1566 """ 1567 if self.accountId is None or not self.accountId: 1568 uLogger.error("Variable `accountId` must be defined for using this method!") 1569 raise Exception("Account ID required") 1570 1571 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1572 1573 self.body = str({"accountId": self.accountId}) 1574 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1575 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1576 1577 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1578 1579 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1581 def RequestStopOrders(self) -> list: 1582 """ 1583 Requesting current actual stop orders for current `accountId`. 1584 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1585 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1586 1587 :return: list of dictionaries with stop orders. 1588 """ 1589 if self.accountId is None or not self.accountId: 1590 uLogger.error("Variable `accountId` must be defined for using this method!") 1591 raise Exception("Account ID required") 1592 1593 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1594 1595 self.body = str({"accountId": self.accountId}) 1596 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1597 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1598 1599 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1600 1601 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1603 def Overview(self, show: bool = False, details: str = "full") -> dict: 1604 """ 1605 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1606 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1607 are defined then also save information to file. 1608 1609 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1610 many requests about the state of the portfolio, and then, based on the received data, a large number 1611 of calculation and statistics are collected. 1612 1613 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1614 :param details: how detailed should the information be? You should specify one of strings: 1615 `full` - shows full available information about portfolio status (by default), 1616 `positions` - shows only open positions, 1617 `digest` - show a short digest of the portfolio status, 1618 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1619 `orders` - shows only sections of open limits and stop orders. 1620 :return: dictionary with client's raw portfolio and some statistics. 1621 """ 1622 if self.accountId is None or not self.accountId: 1623 uLogger.error("Variable `accountId` must be defined for using this method!") 1624 raise Exception("Account ID required") 1625 1626 view = { 1627 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1628 "headers": {}, # list of dictionaries, response headers without "positions" section 1629 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1630 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1631 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1632 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1633 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1634 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1635 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1636 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1637 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1638 }, 1639 "stat": { # --- some statistics calculated using "raw" sections: 1640 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1641 "availableRUB": 0., # available rubles (without other currencies) 1642 "blockedRUB": 0., # blocked sum in Russian Rouble 1643 "totalChangesRUB": 0., # changes for all open trades in RUB 1644 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1645 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1646 "sharesCostRUB": 0., # costs of all shares in RUB 1647 "bondsCostRUB": 0., # costs of all bonds in RUB 1648 "etfsCostRUB": 0., # costs of all etfs in RUB 1649 "futuresCostRUB": 0., # costs of all futures in RUB 1650 "Currencies": [], # list of dictionaries of all currencies statistics 1651 "Shares": [], # list of dictionaries of all shares statistics 1652 "Bonds": [], # list of dictionaries of all bonds statistics 1653 "Etfs": [], # list of dictionaries of all etfs statistics 1654 "Futures": [], # list of dictionaries of all futures statistics 1655 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1656 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1657 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1658 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1659 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1660 }, 1661 "analytics": { # --- some analytics of portfolio: 1662 "distrByAssets": {}, # portfolio distribution by assets 1663 "distrByCompanies": {}, # portfolio distribution by companies 1664 "distrBySectors": {}, # portfolio distribution by sectors 1665 "distrByCurrencies": {}, # portfolio distribution by currencies 1666 "distrByCountries": {}, # portfolio distribution by countries 1667 } 1668 } 1669 1670 details = details.lower() 1671 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1672 if details not in availableDetails: 1673 details = "full" 1674 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1675 1676 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1677 1678 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1679 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1680 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1681 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1682 1683 # save response headers without "positions" section: 1684 for key in portfolioResponse.keys(): 1685 if key != "positions": 1686 view["raw"]["headers"][key] = portfolioResponse[key] 1687 1688 else: 1689 continue 1690 1691 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1692 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1693 for item in portfolioResponse["positions"]: 1694 if item["instrumentType"] == "currency": 1695 self.figi = item["figi"] 1696 curr = self.SearchByFIGI(requestPrice=False) 1697 1698 # current price of currency in RUB: 1699 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1700 "name": curr["name"], 1701 "currentPrice": NanoToFloat( 1702 item["currentPrice"]["units"], 1703 item["currentPrice"]["nano"] 1704 ), 1705 } 1706 1707 view["raw"]["Currencies"].append(item) 1708 1709 elif item["instrumentType"] == "share": 1710 view["raw"]["Shares"].append(item) 1711 1712 elif item["instrumentType"] == "bond": 1713 view["raw"]["Bonds"].append(item) 1714 1715 elif item["instrumentType"] == "etf": 1716 view["raw"]["Etfs"].append(item) 1717 1718 elif item["instrumentType"] == "futures": 1719 view["raw"]["Futures"].append(item) 1720 1721 else: 1722 continue 1723 1724 # how many volume of currencies (by ISO currency name) are blocked: 1725 for item in view["raw"]["positions"]["blocked"]: 1726 blocked = NanoToFloat(item["units"], item["nano"]) 1727 if blocked > 0: 1728 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1729 1730 # how many volume of instruments (by FIGI) are blocked: 1731 for item in view["raw"]["positions"]["securities"]: 1732 blocked = int(item["blocked"]) 1733 if blocked > 0: 1734 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1735 1736 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1737 1738 if "rub" in allBlocked.keys(): 1739 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1740 1741 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1742 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1743 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1744 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1745 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1746 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1747 view["stat"]["portfolioCostRUB"] = sum([ 1748 view["stat"]["allCurrenciesCostRUB"], 1749 view["stat"]["sharesCostRUB"], 1750 view["stat"]["bondsCostRUB"], 1751 view["stat"]["etfsCostRUB"], 1752 view["stat"]["futuresCostRUB"], 1753 ]) 1754 1755 # --- calculating some portfolio statistics: 1756 byComp = {} # distribution by companies 1757 bySect = {} # distribution by sectors 1758 byCurr = {} # distribution by currencies (include RUB) 1759 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1760 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1761 1762 for item in portfolioResponse["positions"]: 1763 self.figi = item["figi"] 1764 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1765 1766 if instrument: 1767 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1768 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1769 1770 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1771 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1772 1773 else: 1774 blocked = 0 1775 1776 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1777 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1778 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1779 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1780 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1781 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1782 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1783 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1784 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1785 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1786 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1787 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1788 1789 statData = { 1790 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1791 "ticker": instrument["ticker"], # ticker by FIGI 1792 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1793 "volume": volume, # available volume of instrument 1794 "lots": lots, # volume in lots of instrument 1795 "direction": direction, # direction of an instrument's position: short or long 1796 "blocked": blocked, # blocked volume of currency or instrument 1797 "currentPrice": curPrice, # current instrument's price in basic asset 1798 "average": average, # current average position price 1799 "cost": cost, # current cost of all volume of instrument in basic asset 1800 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1801 "costRUB": costRUB, # cost of instrument in ruble 1802 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1803 "profit": profit, # expected profit at current moment 1804 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1805 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1806 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1807 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1808 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1809 "step": instrument["step"], # minimum price increment 1810 } 1811 1812 # adding distribution by unique countries: 1813 if statData["country"] not in byCountry.keys(): 1814 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1815 1816 else: 1817 byCountry[statData["country"]]["cost"] += costRUB 1818 byCountry[statData["country"]]["percent"] += percentCostRUB 1819 1820 if item["instrumentType"] != "currency": 1821 # adding distribution by unique companies: 1822 if statData["name"]: 1823 if statData["name"] not in byComp.keys(): 1824 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1825 1826 else: 1827 byComp[statData["name"]]["cost"] += costRUB 1828 byComp[statData["name"]]["percent"] += percentCostRUB 1829 1830 # adding distribution by unique sectors: 1831 if statData["sector"] not in bySect.keys(): 1832 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1833 1834 else: 1835 bySect[statData["sector"]]["cost"] += costRUB 1836 bySect[statData["sector"]]["percent"] += percentCostRUB 1837 1838 # adding distribution by unique currencies: 1839 if currency not in byCurr.keys(): 1840 byCurr[currency] = { 1841 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1842 "cost": costRUB, 1843 "percent": percentCostRUB 1844 } 1845 1846 else: 1847 byCurr[currency]["cost"] += costRUB 1848 byCurr[currency]["percent"] += percentCostRUB 1849 1850 # saving statistics for every instrument: 1851 if item["instrumentType"] == "currency": 1852 view["stat"]["Currencies"].append(statData) 1853 1854 # update dict with free funds for trading (total - blocked) by currencies 1855 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1856 view["stat"]["funds"][currency] = { 1857 "total": volume, 1858 "totalCostRUB": costRUB, # total volume cost in rubles 1859 "free": volume - blocked, 1860 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1861 } 1862 1863 elif item["instrumentType"] == "share": 1864 view["stat"]["Shares"].append(statData) 1865 1866 elif item["instrumentType"] == "bond": 1867 view["stat"]["Bonds"].append(statData) 1868 1869 elif item["instrumentType"] == "etf": 1870 view["stat"]["Etfs"].append(statData) 1871 1872 elif item["instrumentType"] == "Futures": 1873 view["stat"]["Futures"].append(statData) 1874 1875 else: 1876 continue 1877 1878 # total changes in Russian Ruble: 1879 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1880 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1881 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1882 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1883 view["stat"]["funds"]["rub"] = { 1884 "total": view["stat"]["availableRUB"], 1885 "totalCostRUB": view["stat"]["availableRUB"], 1886 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1887 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1888 } 1889 1890 # --- pending orders sector data: 1891 uniquePendingOrders = [] 1892 uniquePendingOrdersFIGIs = [] 1893 for item in view["raw"]["orders"]: 1894 if item["figi"] not in uniquePendingOrdersFIGIs: 1895 uniquePendingOrdersFIGIs.append(item["figi"]) 1896 uniquePendingOrders.append(item) 1897 1898 for item in uniquePendingOrders: 1899 self.figi = item["figi"] 1900 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1901 1902 if instrument: 1903 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1904 orderType = TKS_ORDER_TYPES[item["orderType"]] 1905 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1906 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1907 1908 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1909 if item["direction"] == "ORDER_DIRECTION_BUY": 1910 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1911 1912 else: 1913 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1914 1915 # requested price for order execution: 1916 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1917 1918 # necessary changes in percent to reach target from current price: 1919 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1920 1921 view["stat"]["orders"].append({ 1922 "orderID": item["orderId"], # orderId number parameter of current order 1923 "figi": item["figi"], # FIGI identification 1924 "ticker": instrument["ticker"], # ticker name by FIGI 1925 "lotsRequested": item["lotsRequested"], # requested lots value 1926 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1927 "currentPrice": lastPrice, # current instrument's price for defined action 1928 "targetPrice": target, # requested price for order execution in base currency 1929 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1930 "percentChanges": changes, # changes in percent to target from current price 1931 "currency": item["currency"], # instrument's currency name 1932 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1933 "type": orderType, # type of order from TKS_ORDER_TYPES 1934 "status": orderState, # order status from TKS_ORDER_STATES 1935 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1936 }) 1937 1938 # --- stop orders sector data: 1939 uniqueStopOrders = [] 1940 uniqueStopOrdersFIGIs = [] 1941 for item in view["raw"]["stopOrders"]: 1942 if item["figi"] not in uniqueStopOrdersFIGIs: 1943 uniqueStopOrdersFIGIs.append(item["figi"]) 1944 uniqueStopOrders.append(item) 1945 1946 for item in uniqueStopOrders: 1947 self.figi = item["figi"] 1948 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1949 1950 if instrument: 1951 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1952 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1953 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1954 1955 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1956 if "expirationTime" in item.keys(): 1957 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1958 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1959 1960 else: 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1962 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1963 1964 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1965 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1966 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1967 1968 else: 1969 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1970 1971 # requested price when stop-order executed: 1972 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1973 1974 # price for limit-order, set up when stop-order executed: 1975 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1976 1977 # necessary changes in percent to reach target from current price: 1978 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1979 1980 view["stat"]["stopOrders"].append({ 1981 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1982 "figi": item["figi"], # FIGI identification 1983 "ticker": instrument["ticker"], # ticker name by FIGI 1984 "lotsRequested": item["lotsRequested"], # requested lots value 1985 "currentPrice": lastPrice, # current instrument's price for defined action 1986 "targetPrice": target, # requested price for stop-order execution in base currency 1987 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1988 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1989 "percentChanges": changes, # changes in percent to target from current price 1990 "currency": item["currency"], # instrument's currency name 1991 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1992 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1993 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1994 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1995 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1996 }) 1997 1998 # --- calculating data for analytics section: 1999 # portfolio distribution by assets: 2000 view["analytics"]["distrByAssets"] = { 2001 "Ruble": { 2002 "uniques": 1, 2003 "cost": view["stat"]["availableRUB"], 2004 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2005 }, 2006 "Currencies": { 2007 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2008 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2009 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2010 }, 2011 "Shares": { 2012 "uniques": len(view["stat"]["Shares"]), 2013 "cost": view["stat"]["sharesCostRUB"], 2014 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2015 }, 2016 "Bonds": { 2017 "uniques": len(view["stat"]["Bonds"]), 2018 "cost": view["stat"]["bondsCostRUB"], 2019 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2020 }, 2021 "Etfs": { 2022 "uniques": len(view["stat"]["Etfs"]), 2023 "cost": view["stat"]["etfsCostRUB"], 2024 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2025 }, 2026 "Futures": { 2027 "uniques": len(view["stat"]["Futures"]), 2028 "cost": view["stat"]["futuresCostRUB"], 2029 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2030 }, 2031 } 2032 2033 # portfolio distribution by companies: 2034 view["analytics"]["distrByCompanies"]["All money cash"] = { 2035 "ticker": "", 2036 "cost": view["stat"]["allCurrenciesCostRUB"], 2037 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 } 2039 view["analytics"]["distrByCompanies"].update(byComp) 2040 2041 # portfolio distribution by sectors: 2042 view["analytics"]["distrBySectors"]["All money cash"] = { 2043 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2044 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2045 } 2046 view["analytics"]["distrBySectors"].update(bySect) 2047 2048 # portfolio distribution by currencies: 2049 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2050 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2051 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2052 2053 view["analytics"]["distrByCurrencies"].update(byCurr) 2054 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2055 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2056 2057 # portfolio distribution by countries: 2058 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2059 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2060 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2061 2062 view["analytics"]["distrByCountries"].update(byCountry) 2063 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2065 2066 # --- Prepare text statistics overview in human-readable: 2067 if show: 2068 # Whatever the value `details`, header not changes: 2069 info = [ 2070 "# Client's portfolio\n\n", 2071 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2072 "* **Account ID:** [{}]\n".format(self.accountId), 2073 ] 2074 2075 if details in ["full", "positions", "digest"]: 2076 info.extend([ 2077 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2078 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2079 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2080 view["stat"]["totalChangesRUB"], 2081 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2082 view["stat"]["totalChangesPercentRUB"], 2083 ), 2084 ]) 2085 2086 if details in ["full", "positions"]: 2087 info.extend([ 2088 "## Open positions\n\n", 2089 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2090 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2091 "| Ruble | {:>31} | | | | | |\n".format( 2092 "{:.2f} ({:.2f}) rub".format( 2093 view["stat"]["availableRUB"], 2094 view["stat"]["blockedRUB"], 2095 ) 2096 ) 2097 ]) 2098 2099 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2100 return [ 2101 "| | | | | | | |\n", 2102 "| {:<27} | | | | | {:>19} | |\n".format( 2103 noTradeStr if noTradeStr else typeStr, 2104 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2105 ), 2106 ] 2107 2108 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2109 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2110 "{} [{}]".format(data["ticker"], data["figi"]), 2111 "{:.2f} ({:.2f}) {}".format( 2112 data["volume"], 2113 data["blocked"], 2114 data["currency"], 2115 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2116 data["volume"], 2117 data["blocked"], 2118 ), 2119 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2120 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2121 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2122 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2123 "{}{:.2f} {} ({}{:.2f}%)".format( 2124 "+" if data["profit"] > 0 else "", 2125 data["profit"], data["baseCurrencyName"], 2126 "+" if data["percentProfit"] > 0 else "", 2127 data["percentProfit"], 2128 ), 2129 ) 2130 2131 # --- Show currencies section: 2132 if view["stat"]["Currencies"]: 2133 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2134 for item in view["stat"]["Currencies"]: 2135 info.append(_InfoStr(item, showCurrencyName=True)) 2136 2137 else: 2138 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2139 2140 # --- Show shares section: 2141 if view["stat"]["Shares"]: 2142 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2143 2144 for item in view["stat"]["Shares"]: 2145 info.append(_InfoStr(item)) 2146 2147 else: 2148 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2149 2150 # --- Show bonds section: 2151 if view["stat"]["Bonds"]: 2152 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2153 2154 for item in view["stat"]["Bonds"]: 2155 info.append(_InfoStr(item)) 2156 2157 else: 2158 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2159 2160 # --- Show etfs section: 2161 if view["stat"]["Etfs"]: 2162 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2163 2164 for item in view["stat"]["Etfs"]: 2165 info.append(_InfoStr(item)) 2166 2167 else: 2168 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2169 2170 # --- Show futures section: 2171 if view["stat"]["Futures"]: 2172 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2173 2174 for item in view["stat"]["Futures"]: 2175 info.append(_InfoStr(item)) 2176 2177 else: 2178 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2179 2180 if details in ["full", "orders"]: 2181 # --- Show pending orders section: 2182 if view["stat"]["orders"]: 2183 info.extend([ 2184 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2185 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2186 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2187 ]) 2188 2189 for item in view["stat"]["orders"]: 2190 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2191 "{} [{}]".format(item["ticker"], item["figi"]), 2192 item["orderID"], 2193 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2194 "{} {} ({}{:.2f}%)".format( 2195 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2196 item["baseCurrencyName"], 2197 "+" if item["percentChanges"] > 0 else "", 2198 float(item["percentChanges"]), 2199 ), 2200 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2201 item["action"], 2202 item["type"], 2203 item["date"], 2204 )) 2205 2206 else: 2207 info.append("\n## Total pending limit-orders: 0\n") 2208 2209 # --- Show stop orders section: 2210 if view["stat"]["stopOrders"]: 2211 info.extend([ 2212 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2213 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2214 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2215 ]) 2216 2217 for item in view["stat"]["stopOrders"]: 2218 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2219 "{} [{}]".format(item["ticker"], item["figi"]), 2220 item["orderID"], 2221 item["lotsRequested"], 2222 "{} {} ({}{:.2f}%)".format( 2223 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2224 item["baseCurrencyName"], 2225 "+" if item["percentChanges"] > 0 else "", 2226 float(item["percentChanges"]), 2227 ), 2228 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2229 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2230 item["action"], 2231 item["type"], 2232 item["expType"], 2233 item["createDate"], 2234 item["expDate"], 2235 )) 2236 2237 else: 2238 info.append("\n## Total stop-orders: 0\n") 2239 2240 if details in ["full", "analytics"]: 2241 # -- Show analytics section: 2242 if view["stat"]["portfolioCostRUB"] > 0: 2243 info.extend([ 2244 "\n# Analytics\n" 2245 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2246 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2247 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2248 view["stat"]["totalChangesRUB"], 2249 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2250 view["stat"]["totalChangesPercentRUB"], 2251 ), 2252 "\n## Portfolio distribution by assets\n" 2253 "\n| Type | Uniques | Percent | Current cost |\n", 2254 "|------------|---------|---------|--------------------|\n", 2255 ]) 2256 2257 for key in view["analytics"]["distrByAssets"].keys(): 2258 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2259 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2260 key, 2261 view["analytics"]["distrByAssets"][key]["uniques"], 2262 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2263 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2264 )) 2265 2266 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2267 info.extend([ 2268 "\n## Portfolio distribution by companies\n" 2269 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2270 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2271 ]) 2272 2273 for company in view["analytics"]["distrByCompanies"].keys(): 2274 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2275 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2276 info.append("| {} | {:<7} | {:<18} |\n".format( 2277 "{}{}{}".format( 2278 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2279 company, 2280 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2281 ), 2282 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2283 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2284 )) 2285 2286 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2287 info.extend([ 2288 "\n## Portfolio distribution by sectors\n" 2289 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2290 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2291 ]) 2292 2293 for sector in view["analytics"]["distrBySectors"].keys(): 2294 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2295 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2296 sector, 2297 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2298 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2299 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2300 )) 2301 2302 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2303 info.extend([ 2304 "\n## Portfolio distribution by currencies\n" 2305 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2306 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2307 ]) 2308 2309 for curr in view["analytics"]["distrByCurrencies"].keys(): 2310 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2311 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2312 info.append("| {} | {:<7} | {:<18} |\n".format( 2313 "[{}] {}{}".format( 2314 curr, 2315 view["analytics"]["distrByCurrencies"][curr]["name"], 2316 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2317 ), 2318 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2319 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2320 )) 2321 2322 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2323 info.extend([ 2324 "\n## Portfolio distribution by countries\n" 2325 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2326 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2327 ]) 2328 2329 for country in view["analytics"]["distrByCountries"].keys(): 2330 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2331 nameLen = len(country) 2332 info.append("| {} | {:<7} | {:<18} |\n".format( 2333 "{}{}".format( 2334 country, 2335 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2336 ), 2337 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2338 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2339 )) 2340 2341 infoText = "".join(info) 2342 2343 uLogger.info(infoText) 2344 2345 if details == "full" and self.overviewFile: 2346 filename = self.overviewFile 2347 2348 elif details == "digest" and self.overviewDigestFile: 2349 filename = self.overviewDigestFile 2350 2351 elif details == "positions" and self.overviewPositionsFile: 2352 filename = self.overviewPositionsFile 2353 2354 elif details == "orders" and self.overviewOrdersFile: 2355 filename = self.overviewOrdersFile 2356 2357 elif details == "analytics" and self.overviewAnalyticsFile: 2358 filename = self.overviewAnalyticsFile 2359 2360 else: 2361 filename = "" 2362 2363 if filename: 2364 with open(filename, "w", encoding="UTF-8") as fH: 2365 fH.write(infoText) 2366 2367 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2368 2369 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2371 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2372 """ 2373 Returns history operations between two given dates for current `accountId`. 2374 If `reportFile` string is not empty then also save human-readable report. 2375 Shows some statistical data of closed positions. 2376 2377 :param start: see docstring in `GetDatesAsString()` method 2378 :param end: see docstring in `GetDatesAsString()` method 2379 :param show: if `True` then also prints all records to the console. 2380 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2381 :return: original list of dictionaries with history of deals records from API ("operations" key): 2382 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2383 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2384 """ 2385 if self.accountId is None or not self.accountId: 2386 uLogger.error("Variable `accountId` must be defined for using this method!") 2387 raise Exception("Account ID required") 2388 2389 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2390 2391 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2392 2393 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2394 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2395 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2396 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2397 customStat = {} # custom statistics in additional to responseJSON 2398 2399 # --- output report in human-readable format: 2400 if show or self.reportFile: 2401 splitLine1 = "| | | | | |\n" # Summary section 2402 splitLine2 = "| | | | | | | | |\n" # Operations section 2403 nextDay = "" 2404 2405 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2406 2407 if len(ops) > 0: 2408 customStat = { 2409 "opsCount": 0, # total operations count 2410 "buyCount": 0, # buy operations 2411 "sellCount": 0, # sell operations 2412 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2413 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2414 "payIn": {"rub": 0.}, # Deposit brokerage account 2415 "payOut": {"rub": 0.}, # Withdrawals 2416 "divs": {"rub": 0.}, # Dividends income 2417 "coupons": {"rub": 0.}, # Coupon's income 2418 "brokerCom": {"rub": 0.}, # Service commissions 2419 "serviceCom": {"rub": 0.}, # Service commissions 2420 "marginCom": {"rub": 0.}, # Margin commissions 2421 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2422 } 2423 2424 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2425 for item in ops: 2426 if item["state"] == "OPERATION_STATE_EXECUTED": 2427 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2428 2429 # count buy operations: 2430 if "_BUY" in item["operationType"]: 2431 customStat["buyCount"] += 1 2432 2433 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2434 customStat["buyTotal"][item["payment"]["currency"]] += payment 2435 2436 else: 2437 customStat["buyTotal"][item["payment"]["currency"]] = payment 2438 2439 # count sell operations: 2440 elif "_SELL" in item["operationType"]: 2441 customStat["sellCount"] += 1 2442 2443 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2444 customStat["sellTotal"][item["payment"]["currency"]] += payment 2445 2446 else: 2447 customStat["sellTotal"][item["payment"]["currency"]] = payment 2448 2449 # count incoming operations: 2450 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2451 if item["payment"]["currency"] in customStat["payIn"].keys(): 2452 customStat["payIn"][item["payment"]["currency"]] += payment 2453 2454 else: 2455 customStat["payIn"][item["payment"]["currency"]] = payment 2456 2457 # count withdrawals operations: 2458 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2459 if item["payment"]["currency"] in customStat["payOut"].keys(): 2460 customStat["payOut"][item["payment"]["currency"]] += payment 2461 2462 else: 2463 customStat["payOut"][item["payment"]["currency"]] = payment 2464 2465 # count dividends income: 2466 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2467 if item["payment"]["currency"] in customStat["divs"].keys(): 2468 customStat["divs"][item["payment"]["currency"]] += payment 2469 2470 else: 2471 customStat["divs"][item["payment"]["currency"]] = payment 2472 2473 # count coupon's income: 2474 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2475 if item["payment"]["currency"] in customStat["coupons"].keys(): 2476 customStat["coupons"][item["payment"]["currency"]] += payment 2477 2478 else: 2479 customStat["coupons"][item["payment"]["currency"]] = payment 2480 2481 # count broker commissions: 2482 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2483 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2484 customStat["brokerCom"][item["payment"]["currency"]] += payment 2485 2486 else: 2487 customStat["brokerCom"][item["payment"]["currency"]] = payment 2488 2489 # count service commissions: 2490 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2491 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2492 customStat["serviceCom"][item["payment"]["currency"]] += payment 2493 2494 else: 2495 customStat["serviceCom"][item["payment"]["currency"]] = payment 2496 2497 # count margin commissions: 2498 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2499 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2500 customStat["marginCom"][item["payment"]["currency"]] += payment 2501 2502 else: 2503 customStat["marginCom"][item["payment"]["currency"]] = payment 2504 2505 # count withholding taxes: 2506 elif "_TAX" in item["operationType"]: 2507 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2508 customStat["allTaxes"][item["payment"]["currency"]] += payment 2509 2510 else: 2511 customStat["allTaxes"][item["payment"]["currency"]] = payment 2512 2513 else: 2514 continue 2515 2516 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2517 2518 # --- view "Actions" lines: 2519 info.extend([ 2520 "| Report sections | | | | |\n", 2521 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2522 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2523 "| | Buy: {:<22} | {:<28} | | |\n".format( 2524 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2525 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2526 ), 2527 "| | Sell: {:<21} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2530 ), 2531 ]) 2532 2533 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2534 for key in opsKeys: 2535 if key == "rub": 2536 continue 2537 2538 info.extend([ 2539 "| | | {:<28} | | |\n".format( 2540 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2541 ), 2542 "| | | {:<28} | | |\n".format( 2543 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2544 ), 2545 ]) 2546 2547 info.append(splitLine1) 2548 2549 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2550 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2551 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2552 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2553 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2554 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2555 ) 2556 2557 # --- view "Payments" lines: 2558 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2559 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2560 2561 for key in paymentsKeys: 2562 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2563 2564 info.append(splitLine1) 2565 2566 # --- view "Commissions and taxes" lines: 2567 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2568 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2569 2570 for key in comKeys: 2571 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2572 2573 info.append(splitLine1) 2574 2575 info.extend([ 2576 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2577 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2578 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2579 ]) 2580 2581 else: 2582 info.append("Broker returned no operations during this period\n") 2583 2584 # --- view "Operations" section: 2585 for item in ops: 2586 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2587 continue 2588 2589 else: 2590 self.figi = item["figi"] if item["figi"] else "" 2591 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2592 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2593 2594 # group of deals during one day: 2595 if nextDay and item["date"].split("T")[0] != nextDay: 2596 info.append(splitLine2) 2597 nextDay = "" 2598 2599 else: 2600 nextDay = item["date"].split("T")[0] # saving current day for splitting 2601 2602 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2603 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2604 self.figi if self.figi else "—", 2605 instrument["ticker"] if instrument else "—", 2606 instrument["type"] if instrument else "—", 2607 item["quantity"] if int(item["quantity"]) > 0 else "—", 2608 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2609 TKS_OPERATION_STATES[item["state"]], 2610 TKS_OPERATION_TYPES[item["operationType"]], 2611 )) 2612 2613 infoText = "".join(info) 2614 2615 if show: 2616 if self.moreDebug: 2617 uLogger.debug("Records about history of a client's operations successfully received") 2618 2619 uLogger.info(infoText) 2620 2621 if self.reportFile: 2622 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2623 fH.write(infoText) 2624 2625 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2626 2627 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2629 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2630 """ 2631 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2632 2633 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2634 Warning! Broker server used ISO UTC time by default. 2635 2636 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2637 Also, `historyFile` used to update history with `onlyMissing` parameter. 2638 2639 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2640 2641 :param start: see docstring in `GetDatesAsString()` method. 2642 :param end: see docstring in `GetDatesAsString()` method. 2643 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2644 `"hour"`, `"day"`. Default: `"hour"`. 2645 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2646 False by default. Warning! History appends only from last candle to current time 2647 with always update last candle! 2648 :param csvSep: separator if csv-file is used, `,` by default. 2649 :param show: if `True` then also prints Pandas DataFrame to the console. 2650 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2651 `["date", "time", "open", "high", "low", "close", "volume"]`. 2652 """ 2653 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2654 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2655 history = None # empty pandas object for history 2656 2657 if interval not in TKS_CANDLE_INTERVALS.keys(): 2658 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2659 raise Exception("Incorrect value") 2660 2661 if not (self.ticker or self.figi): 2662 uLogger.error("Ticker or FIGI must be defined!") 2663 raise Exception("Ticker or FIGI required") 2664 2665 if self.ticker and not self.figi: 2666 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2667 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2668 2669 if self.figi and not self.ticker: 2670 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2671 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2672 2673 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2674 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2675 if interval.lower() != "day": 2676 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2677 2678 delta = dtEnd - dtStart # current UTC time minus last time in file 2679 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2680 2681 # calculate history length in candles: 2682 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2683 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2684 length += 1 # to avoid fraction time 2685 2686 # calculate data blocks count: 2687 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2688 2689 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2690 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2691 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2692 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2693 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2694 2695 tempOld = None # pandas object for old history, if --only-missing key present 2696 lastTime = None # datetime object of last old candle in file 2697 2698 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2699 uLogger.debug("--only-missing key present, add only last missing candles...") 2700 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2701 2702 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2703 2704 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2705 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2706 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2707 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2708 2709 # get last datetime object from last string in file or minus 1 delta if file is empty: 2710 if len(tempOld) > 0: 2711 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2712 2713 else: 2714 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2715 2716 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2717 2718 responseJSONs = [] # raw history blocks of data 2719 2720 blockEnd = dtEnd 2721 for item in range(blocks): 2722 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2723 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2724 2725 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2726 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2727 )) 2728 2729 if blockStart == blockEnd: 2730 uLogger.debug("Skipped this zero-length block...") 2731 2732 else: 2733 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2734 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2735 self.body = str({ 2736 "figi": self.figi, 2737 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2738 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 "interval": TKS_CANDLE_INTERVALS[interval][0] 2740 }) 2741 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2742 2743 if "code" in responseJSON.keys(): 2744 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2745 2746 else: 2747 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2748 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2749 2750 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2751 2752 blockEnd = blockStart 2753 2754 printCount = len(responseJSONs) # candles to show in console 2755 if responseJSONs: 2756 tempHistory = pd.DataFrame( 2757 data={ 2758 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2759 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2761 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2762 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2763 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2764 "volume": [int(item["volume"]) for item in responseJSONs], 2765 }, 2766 index=range(len(responseJSONs)), 2767 columns=["date", "time", "open", "high", "low", "close", "volume"], 2768 ) 2769 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2770 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2771 2772 # append only newest candles to old history if --only-missing key present: 2773 if onlyMissing and tempOld is not None and lastTime is not None: 2774 index = 0 # find start index in tempHistory data: 2775 2776 for i, item in tempHistory.iterrows(): 2777 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2778 2779 if curTime == lastTime: 2780 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2781 index = i 2782 printCount = index + 1 2783 break 2784 2785 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2786 2787 else: 2788 history = tempHistory # if no `--only-missing` key then load full data from server 2789 2790 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2791 2792 if history is not None and not history.empty: 2793 if show: 2794 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2795 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2796 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2797 )) 2798 2799 else: 2800 uLogger.warning("Received an empty candles history!") 2801 2802 if self.historyFile is not None: 2803 if history is not None and not history.empty: 2804 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2805 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2806 2807 else: 2808 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2809 2810 else: 2811 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2812 2813 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2815 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2816 """ 2817 Load candles history from csv-file and return Pandas DataFrame object. 2818 2819 See also: `History()` and `ShowHistoryChart()` methods. 2820 2821 :param filePath: path to csv-file to open. 2822 """ 2823 loadedHistory = None # init candles data object 2824 2825 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2826 2827 if os.path.exists(filePath): 2828 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2829 2830 tfStr = self.priceModel.FormattedDelta( 2831 self.priceModel.timeframe, 2832 "{days} days {hours}h {minutes}m {seconds}s", 2833 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2834 self.priceModel.timeframe, 2835 "{hours}h {minutes}m {seconds}s", 2836 ) 2837 2838 if loadedHistory is not None and not loadedHistory.empty: 2839 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2840 len(loadedHistory), 2841 tfStr, 2842 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2843 ) 2844 2845 else: 2846 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2847 2848 else: 2849 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2850 2851 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2853 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2854 """ 2855 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2856 2857 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2858 Default: `index.html` (both for interact and non-interact candlesticks chart). 2859 2860 See also: `History()` and `LoadHistory()` methods. 2861 2862 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2863 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2864 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2865 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2866 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2867 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2868 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2869 """ 2870 if isinstance(candles, str): 2871 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2872 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2873 2874 elif isinstance(candles, pd.DataFrame): 2875 self.priceModel.prices = candles # set candles chain from variable 2876 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2877 2878 if "datetime" not in candles.columns: 2879 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2880 2881 else: 2882 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2883 raise Exception("Incorrect value") 2884 2885 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2886 2887 if interact: 2888 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2889 2890 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2891 2892 else: 2893 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2894 2895 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2896 2897 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2899 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2900 """ 2901 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2902 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2903 2904 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2905 2906 :param operation: string "Buy" or "Sell". 2907 :param lots: volume, integer count of lots >= 1. 2908 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2909 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2910 :param expDate: string "Undefined" by default or local date in future, 2911 it is a string with format `%Y-%m-%d %H:%M:%S`. 2912 :return: JSON with response from broker server. 2913 """ 2914 if self.accountId is None or not self.accountId: 2915 uLogger.error("Variable `accountId` must be defined for using this method!") 2916 raise Exception("Account ID required") 2917 2918 if operation is None or not operation or operation not in ("Buy", "Sell"): 2919 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2920 raise Exception("Incorrect value") 2921 2922 if lots is None or lots < 1: 2923 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2924 lots = 1 2925 2926 if tp is None or tp < 0: 2927 tp = 0 2928 2929 if sl is None or sl < 0: 2930 sl = 0 2931 2932 if expDate is None or not expDate: 2933 expDate = "Undefined" 2934 2935 if not (self.ticker or self.figi): 2936 uLogger.error("Ticker or FIGI must be defined!") 2937 raise Exception("Ticker or FIGI required") 2938 2939 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2940 self.ticker = instrument["ticker"] 2941 self.figi = instrument["figi"] 2942 2943 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2944 2945 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2946 self.body = str({ 2947 "figi": self.figi, 2948 "quantity": str(lots), 2949 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2950 "accountId": str(self.accountId), 2951 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2952 }) 2953 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2954 2955 if "orderId" in response.keys(): 2956 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2957 operation, response["orderId"], 2958 self.ticker, self.figi, lots, 2959 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2960 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2961 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2962 )) 2963 2964 else: 2965 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 2966 2967 if tp > 0: 2968 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2969 2970 if sl > 0: 2971 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2972 2973 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2975 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2976 """ 2977 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2978 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2979 2980 See also: `Order()` and `Trade()` docstrings. 2981 2982 :param lots: volume, integer count of lots >= 1. 2983 :param tp: float > 0, take profit price of stop-order. 2984 :param sl: float > 0, stop loss price of stop-order. 2985 :param expDate: it's a local date in future. 2986 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2987 :return: JSON with response from broker server. 2988 """ 2989 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2991 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2992 """ 2993 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2994 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2995 2996 See also: `Order()` and `Trade()` docstrings. 2997 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, take profit price of stop-order. 3000 :param sl: float > 0, stop loss price of stop-order. 3001 :param expDate: it's a local date in the future. 3002 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3007 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3008 """ 3009 Close position of given instruments. 3010 3011 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3012 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3013 This avoids unnecessary downloading data from the server. 3014 """ 3015 if instruments is None or not instruments: 3016 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3017 raise Exception("Ticker or FIGI required") 3018 3019 if isinstance(instruments, str): 3020 instruments = [instruments] 3021 3022 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3023 if uniqueInstruments: 3024 if portfolio is None or not portfolio: 3025 portfolio = self.Overview(show=False) 3026 3027 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3028 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3029 3030 for self.figi in uniqueInstruments: 3031 if self.figi not in allOpened: 3032 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3033 continue 3034 3035 # search open trade info about instrument by ticker: 3036 instrument = {} 3037 for iType in TKS_INSTRUMENTS: 3038 if instrument: 3039 break 3040 3041 for item in portfolio["stat"][iType]: 3042 if item["figi"] == self.figi: 3043 instrument = item 3044 break 3045 3046 if instrument: 3047 self.ticker = instrument["ticker"] 3048 self.figi = instrument["figi"] 3049 3050 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3051 self.ticker, 3052 self.figi, 3053 int(instrument["volume"]), 3054 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3055 )) 3056 3057 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3058 3059 if tradeLots > 0: 3060 if instrument["blocked"] > 0: 3061 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3062 instrument["blocked"], 3063 self.ticker, 3064 tradeLots, 3065 )) 3066 3067 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3068 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3069 3070 else: 3071 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3073 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3074 """ 3075 Close all positions of given instruments with defined type. 3076 3077 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3078 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3079 This avoids unnecessary downloading data from the server. 3080 """ 3081 if iType not in TKS_INSTRUMENTS: 3082 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3083 3084 else: 3085 if portfolio is None or not portfolio: 3086 portfolio = self.Overview(show=False) 3087 3088 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3089 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3090 3091 if tickers and portfolio: 3092 self.CloseTrades(tickers, portfolio) 3093 3094 else: 3095 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3097 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3098 """ 3099 Universal method to create market or limit orders with all available parameters for current `accountId`. 3100 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3101 3102 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3103 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3104 3105 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3106 then broker immediately open market order as you can do simple --buy or --sell operations! 3107 3108 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3109 When current price will go up or down to target price value then broker opens a limit order. 3110 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3111 3112 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3113 3114 :param operation: string "Buy" or "Sell". 3115 :param orderType: string "Limit" or "Stop". 3116 :param lots: volume, integer count of lots >= 1. 3117 :param targetPrice: target price > 0. This is open trade price for limit order. 3118 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3119 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3120 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3121 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3122 Stop loss order always executed by market price. 3123 :param expDate: string "Undefined" by default or local date in future. 3124 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3125 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3126 A limit order has no expiration date, it lasts until the end of the trading day. 3127 :return: JSON with response from broker server. 3128 """ 3129 if self.accountId is None or not self.accountId: 3130 uLogger.error("Variable `accountId` must be defined for using this method!") 3131 raise Exception("Account ID required") 3132 3133 if operation is None or not operation or operation not in ("Buy", "Sell"): 3134 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3135 raise Exception("Incorrect value") 3136 3137 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3138 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3139 raise Exception("Incorrect value") 3140 3141 if lots is None or lots < 1: 3142 uLogger.error("You must define trade volume > 0: integer count of lots!") 3143 raise Exception("Incorrect value") 3144 3145 if targetPrice is None or targetPrice <= 0: 3146 uLogger.error("Target price for limit-order must be greater than 0!") 3147 raise Exception("Incorrect value") 3148 3149 if limitPrice is None or limitPrice <= 0: 3150 limitPrice = targetPrice 3151 3152 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3153 stopType = "Limit" 3154 3155 if expDate is None or not expDate: 3156 expDate = "Undefined" 3157 3158 if not (self.ticker or self.figi): 3159 uLogger.error("Tocker or FIGI must be defined!") 3160 raise Exception("Ticker or FIGI required") 3161 3162 response = {} 3163 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3164 self.ticker = instrument["ticker"] 3165 self.figi = instrument["figi"] 3166 3167 if orderType == "Limit": 3168 uLogger.debug( 3169 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3170 self.ticker, self.figi, 3171 operation, lots, targetPrice, instrument["currency"], 3172 )) 3173 3174 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3175 self.body = str({ 3176 "figi": self.figi, 3177 "quantity": str(lots), 3178 "price": FloatToNano(targetPrice), 3179 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3180 "accountId": str(self.accountId), 3181 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3182 }) 3183 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3184 3185 if "orderId" in response.keys(): 3186 uLogger.info( 3187 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3188 response["orderId"], 3189 self.ticker, self.figi, 3190 operation, lots, targetPrice, instrument["currency"], 3191 )) 3192 3193 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3194 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3195 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3196 targetPrice, instrument["currency"], 3197 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3198 )) 3199 3200 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3201 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3202 targetPrice, instrument["currency"], 3203 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3204 )) 3205 3206 else: 3207 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3208 3209 if orderType == "Stop": 3210 uLogger.debug( 3211 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3212 self.ticker, self.figi, 3213 operation, lots, 3214 targetPrice, instrument["currency"], 3215 limitPrice, instrument["currency"], 3216 stopType, expDate, 3217 )) 3218 3219 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3220 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3221 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3222 3223 body = { 3224 "figi": self.figi, 3225 "quantity": str(lots), 3226 "price": FloatToNano(limitPrice), 3227 "stopPrice": FloatToNano(targetPrice), 3228 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3229 "accountId": str(self.accountId), 3230 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3231 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3232 } 3233 3234 if expDateUTC: 3235 body["expireDate"] = expDateUTC 3236 3237 self.body = str(body) 3238 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3239 3240 if "stopOrderId" in response.keys(): 3241 uLogger.info( 3242 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3243 response["stopOrderId"], 3244 self.ticker, self.figi, 3245 operation, lots, 3246 targetPrice, instrument["currency"], 3247 limitPrice, instrument["currency"], 3248 TKS_STOP_ORDER_TYPES[stopOrderType], 3249 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3250 )) 3251 3252 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3253 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3254 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3255 targetPrice, instrument["currency"], 3256 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3257 )) 3258 3259 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3260 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3261 targetPrice, instrument["currency"], 3262 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3263 )) 3264 3265 else: 3266 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3267 3268 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3270 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3271 """ 3272 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3273 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3274 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3275 See also: `Order()` docstring. 3276 3277 :param lots: volume, integer count of lots >= 1. 3278 :param targetPrice: target price > 0. This is open trade price for limit order. 3279 :return: JSON with response from broker server. 3280 """ 3281 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3283 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3284 """ 3285 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3286 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3287 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3288 target price value then broker opens a limit order. See also: `Order()` docstring. 3289 3290 :param lots: volume, integer count of lots >= 1. 3291 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3292 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3293 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3294 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3295 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3296 :param expDate: string "Undefined" by default or local date in future. 3297 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3298 This date is converting to UTC format for server. 3299 :return: JSON with response from broker server. 3300 """ 3301 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3303 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3304 """ 3305 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3306 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3307 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3308 See also: `Order()` docstring. 3309 3310 :param lots: volume, integer count of lots >= 1. 3311 :param targetPrice: target price > 0. This is open trade price for limit order. 3312 :return: JSON with response from broker server. 3313 """ 3314 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3316 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3317 """ 3318 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3319 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3320 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3321 target price value then broker opens a limit order. See also: `Order()` docstring. 3322 3323 :param lots: volume, integer count of lots >= 1. 3324 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3325 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3326 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3327 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3328 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3329 :param expDate: string "Undefined" by default or local date in future. 3330 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3331 This date is converting to UTC format for server. 3332 :return: JSON with response from broker server. 3333 """ 3334 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3336 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3337 """ 3338 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3339 3340 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3341 :param allOrdersIDs: pre-received lists of all active pending orders. 3342 This avoids unnecessary downloading data from the server. 3343 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3344 """ 3345 if self.accountId is None or not self.accountId: 3346 uLogger.error("Variable `accountId` must be defined for using this method!") 3347 raise Exception("Account ID required") 3348 3349 if orderIDs: 3350 if allOrdersIDs is None or not allOrdersIDs: 3351 rawOrders = self.RequestPendingOrders() 3352 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3353 3354 if allStopOrdersIDs is None or not allStopOrdersIDs: 3355 rawStopOrders = self.RequestStopOrders() 3356 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3357 3358 for orderID in orderIDs: 3359 idInPendingOrders = orderID in allOrdersIDs 3360 idInStopOrders = orderID in allStopOrdersIDs 3361 3362 if not (idInPendingOrders or idInStopOrders): 3363 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3364 continue 3365 3366 else: 3367 if idInPendingOrders: 3368 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3369 3370 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3371 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3372 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3373 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3374 3375 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3376 if self.moreDebug: 3377 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3378 3379 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3380 3381 else: 3382 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3383 3384 elif idInStopOrders: 3385 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3386 3387 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3388 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3389 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3390 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3391 3392 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3393 if self.moreDebug: 3394 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3395 3396 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3397 3398 else: 3399 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3400 3401 else: 3402 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3404 def CloseAllOrders(self) -> None: 3405 """ 3406 Gets a list of open pending and stop orders and cancel it all. 3407 """ 3408 rawOrders = self.RequestPendingOrders() 3409 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3410 lenOrders = len(allOrdersIDs) 3411 3412 rawStopOrders = self.RequestStopOrders() 3413 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3414 lenSOrders = len(allStopOrdersIDs) 3415 3416 if lenOrders > 0 or lenSOrders > 0: 3417 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3418 3419 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3420 3421 else: 3422 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3424 def CloseAll(self, *args) -> None: 3425 """ 3426 Close all available (not blocked) opened trades and orders. 3427 3428 Also, you can select one or more keywords case-insensitive: 3429 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3430 3431 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3432 """ 3433 overview = self.Overview(show=False) # get all open trades info 3434 3435 if len(args) == 0: 3436 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3437 self.CloseAllOrders() # close all pending and stop orders 3438 3439 for iType in TKS_INSTRUMENTS: 3440 if iType != "Currencies": 3441 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3442 3443 else: 3444 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3445 lowerArgs = [x.lower() for x in args] 3446 3447 if "orders" in lowerArgs: 3448 self.CloseAllOrders() # close all pending and stop orders 3449 3450 for iType in TKS_INSTRUMENTS: 3451 if iType.lower() in lowerArgs and iType != "Currencies": 3452 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3454 @staticmethod 3455 def ParseOrderParameters(operation, **inputParameters): 3456 """ 3457 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3458 3459 :param operation: string "Buy" or "Sell". 3460 :param inputParameters: this is dict of strings that looks like this 3461 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3462 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3463 "prices" key: one or more prices to open limit-orders 3464 Counts of values in lots and prices lists must be equals! 3465 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3466 """ 3467 # TODO: update order grid work with api v2 3468 pass 3469 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3470 # 3471 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3472 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3473 # raise Exception("Incorrect value") 3474 # 3475 # if "l" in inputParameters.keys(): 3476 # inputParameters["lots"] = inputParameters.pop("l") 3477 # 3478 # if "p" in inputParameters.keys(): 3479 # inputParameters["prices"] = inputParameters.pop("p") 3480 # 3481 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3482 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3483 # raise Exception("Incorrect value") 3484 # 3485 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3486 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3487 # 3488 # if len(lots) != len(prices): 3489 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3490 # raise Exception("Incorrect value") 3491 # 3492 # uLogger.debug("Extracted parameters for orders:") 3493 # uLogger.debug("lots = {}".format(lots)) 3494 # uLogger.debug("prices = {}".format(prices)) 3495 # 3496 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3497 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3498 # uLogger.debug("Order parameters: {}".format(result)) 3499 # 3500 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3502 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3503 """ 3504 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3505 3506 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3507 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3508 """ 3509 result = False 3510 msg = "Instrument not defined!" 3511 3512 if portfolio is None or not portfolio: 3513 portfolio = self.Overview(show=False) 3514 3515 if self.ticker: 3516 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3517 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3518 3519 for iType in TKS_INSTRUMENTS: 3520 for instrument in portfolio["stat"][iType]: 3521 if instrument["ticker"] == self.ticker: 3522 result = True 3523 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3524 break 3525 3526 elif self.figi: 3527 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3528 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3529 3530 for iType in TKS_INSTRUMENTS: 3531 for instrument in portfolio["stat"][iType]: 3532 if instrument["figi"] == self.figi: 3533 result = True 3534 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3535 break 3536 3537 else: 3538 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3539 3540 uLogger.debug(msg) 3541 3542 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3544 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3545 """ 3546 Returns instrument is in the user's portfolio if it presents there. 3547 Instrument must be defined by `ticker` (highly priority) or `figi`. 3548 3549 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3550 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3551 """ 3552 result = None 3553 msg = "Instrument not defined!" 3554 3555 if portfolio is None or not portfolio: 3556 portfolio = self.Overview(show=False) 3557 3558 if self.ticker: 3559 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3560 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3561 3562 for iType in TKS_INSTRUMENTS: 3563 for instrument in portfolio["stat"][iType]: 3564 if instrument["ticker"] == self.ticker: 3565 result = instrument 3566 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3567 break 3568 3569 elif self.figi: 3570 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3571 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3572 3573 for iType in TKS_INSTRUMENTS: 3574 for instrument in portfolio["stat"][iType]: 3575 if instrument["figi"] == self.figi: 3576 result = instrument 3577 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3578 break 3579 3580 else: 3581 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3582 3583 uLogger.debug(msg) 3584 3585 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3587 def RequestLimits(self) -> dict: 3588 """ 3589 Method for obtaining the available funds for withdrawal for current `accountId`. 3590 3591 See also: 3592 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3593 - `OverviewLimits()` method 3594 3595 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3596 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3597 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3598 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3599 """ 3600 if self.accountId is None or not self.accountId: 3601 uLogger.error("Variable `accountId` must be defined for using this method!") 3602 raise Exception("Account ID required") 3603 3604 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3605 3606 self.body = str({"accountId": self.accountId}) 3607 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3608 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3609 3610 if self.moreDebug: 3611 uLogger.debug("Records about available funds for withdrawal successfully received") 3612 3613 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3615 def OverviewLimits(self, show: bool = False) -> dict: 3616 """ 3617 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3618 3619 See also: `RequestLimits()`. 3620 3621 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3622 :return: dict with raw parsed data from server and some calculated statistics about it. 3623 """ 3624 if self.accountId is None or not self.accountId: 3625 uLogger.error("Variable `accountId` must be defined for using this method!") 3626 raise Exception("Account ID required") 3627 3628 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3629 3630 view = { 3631 "rawLimits": rawLimits, 3632 "limits": { # parsed data for every currency: 3633 "money": { # this is an array of portfolio currency positions 3634 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3635 }, 3636 "blocked": { # this is an array of blocked currency 3637 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3638 }, 3639 "blockedGuarantee": { # this is locked money under collateral for futures 3640 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3641 }, 3642 }, 3643 } 3644 3645 # --- Prepare text table with limits in human-readable format: 3646 if show: 3647 info = [ 3648 "# Withdrawal limits\n\n", 3649 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3650 "* **Account ID:** [{}]\n".format(self.accountId), 3651 ] 3652 3653 if view["limits"]["money"]: 3654 info.extend([ 3655 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3656 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3657 ]) 3658 3659 else: 3660 info.append("\nNo withdrawal limits\n") 3661 3662 for curr in view["limits"]["money"].keys(): 3663 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3664 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3665 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3666 3667 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3668 "[{}]".format(curr), 3669 "{:.2f}".format(view["limits"]["money"][curr]), 3670 "{:.2f}".format(availableMoney), 3671 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3672 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3673 ) 3674 3675 if curr == "rub": 3676 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3677 3678 else: 3679 info.append(infoStr) 3680 3681 infoText = "".join(info) 3682 3683 uLogger.info(infoText) 3684 3685 if self.withdrawalLimitsFile: 3686 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3687 fH.write(infoText) 3688 3689 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3690 3691 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3693 def RequestAccounts(self) -> dict: 3694 """ 3695 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3696 3697 See also: 3698 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3699 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3700 - `OverviewUserInfo()` method 3701 3702 :return: dict with raw data from server that contains accounts info. Example of dict: 3703 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3704 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3705 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3706 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3707 """ 3708 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3709 3710 self.body = str({}) 3711 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3712 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3713 3714 if self.moreDebug: 3715 uLogger.debug("Records about available accounts successfully received") 3716 3717 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3719 def RequestUserInfo(self) -> dict: 3720 """ 3721 Method for requesting common user's information. 3722 3723 See also: 3724 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3725 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3726 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3727 - `OverviewUserInfo()` method 3728 3729 :return: dict with raw data from server that contains user's information. Example of dict: 3730 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3731 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3732 """ 3733 uLogger.debug("Requesting common user's information. Wait, please...") 3734 3735 self.body = str({}) 3736 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3737 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3738 3739 if self.moreDebug: 3740 uLogger.debug("Records about current user successfully received") 3741 3742 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3744 def RequestMarginStatus(self, accountId: str = None) -> dict: 3745 """ 3746 Method for requesting margin calculation for defined account ID. 3747 3748 See also: 3749 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3750 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3751 - `OverviewUserInfo()` method 3752 3753 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3754 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3755 Example of responses: 3756 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3757 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3758 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3759 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3760 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3761 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3762 """ 3763 if accountId is None or not accountId: 3764 if self.accountId is None or not self.accountId: 3765 uLogger.error("Variable `accountId` must be defined for using this method!") 3766 raise Exception("Account ID required") 3767 3768 else: 3769 accountId = self.accountId # use `self.accountId` (main ID) by default 3770 3771 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3772 3773 self.body = str({"accountId": accountId}) 3774 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3775 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3776 3777 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3778 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3779 rawMargin = {} 3780 3781 else: 3782 if self.moreDebug: 3783 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3784 3785 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3787 def RequestTariffLimits(self) -> dict: 3788 """ 3789 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3790 3791 See also: 3792 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3793 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3794 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3795 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3796 - `OverviewUserInfo()` method 3797 3798 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3799 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3800 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3801 """ 3802 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3803 3804 self.body = str({}) 3805 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3806 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3807 3808 if self.moreDebug: 3809 uLogger.debug("Records with limits of current tariff successfully received") 3810 3811 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3813 def RequestBondCoupons(self, iJSON: dict) -> dict: 3814 """ 3815 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3816 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3817 All dates are in UTC timezone. 3818 3819 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3820 Documentation: 3821 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3822 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3823 3824 See also: `ExtendBondsData()`. 3825 3826 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3827 If raw iJSON is not data of bond then server returns an error [400] with message: 3828 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3829 :return: dictionary with bond payment calendar. Response example 3830 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3831 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3832 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3833 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3834 """ 3835 if iJSON["figi"] is None or not iJSON["figi"]: 3836 uLogger.error("FIGI must be defined for using this method!") 3837 raise Exception("FIGI required") 3838 3839 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3840 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3841 3842 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3843 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3844 self.figi, 3845 startDate, 3846 endDate, 3847 )) 3848 3849 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3850 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3851 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3852 3853 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3854 uLogger.warning("Instrument type is not bond!") 3855 3856 else: 3857 if self.moreDebug: 3858 uLogger.debug("Records about bond payment calendar successfully received") 3859 3860 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3862 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3863 """ 3864 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3865 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3866 coupon yields, current yields and some statistics etc. 3867 3868 WARNING! This is too long operation if a lot of bonds requested from broker server. 3869 3870 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3871 3872 :param instruments: list of strings with tickers or FIGIs. 3873 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3874 for further used by data scientists or stock analytics. 3875 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3876 In XLSX-file and Pandas DataFrame fields mean: 3877 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3878 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3879 """ 3880 if instruments is None or not instruments: 3881 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3882 raise Exception("Ticker or FIGI required") 3883 3884 if isinstance(instruments, str): 3885 instruments = [instruments] 3886 3887 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3888 3889 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3890 3891 iCount = len(uniqueInstruments) 3892 tooLong = iCount >= 20 3893 if tooLong: 3894 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3895 3896 bonds = None 3897 for i, self.figi in enumerate(uniqueInstruments): 3898 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3899 3900 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3901 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3902 rawBond = self.SearchByFIGI(requestPrice=True) 3903 3904 # Widen raw data with UTC current time (iData["actualDateTime"]): 3905 actualDate = datetime.now(tzutc()) 3906 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3907 3908 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3909 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3910 3911 # Replace some values with human-readable: 3912 iData["nominalCurrency"] = iData["nominal"]["currency"] 3913 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3914 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3915 iData["aciCurrency"] = iData["aciValue"]["currency"] 3916 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3917 iData["issueSize"] = int(iData["issueSize"]) 3918 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3919 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3920 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3921 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3922 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3923 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3924 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3925 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3926 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3927 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3928 3929 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3930 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3931 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3932 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3933 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3934 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3935 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3936 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3937 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3938 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3939 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3940 3941 # Widen raw data with calendar data from `rawCalendar` values: 3942 calendarData = [] 3943 if "events" in iData["rawCalendar"].keys(): 3944 for item in iData["rawCalendar"]["events"]: 3945 calendarData.append({ 3946 "couponDate": item["couponDate"], 3947 "couponNumber": int(item["couponNumber"]), 3948 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3949 "payCurrency": item["payOneBond"]["currency"], 3950 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3951 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3952 "couponStartDate": item["couponStartDate"], 3953 "couponEndDate": item["couponEndDate"], 3954 "couponPeriod": item["couponPeriod"], 3955 }) 3956 3957 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3958 if "maturityDate" not in iData.keys(): 3959 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3960 3961 # Widen raw data with Coupon Rate. 3962 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3963 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3964 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3965 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3966 3967 # Widen raw data with Yield to Maturity (YTM) on current date. 3968 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3969 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3970 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3971 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3972 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3973 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3974 3975 iData["calendar"] = calendarData # adds calendar at the end 3976 3977 # Remove not used data: 3978 iData.pop("uid") 3979 iData.pop("positionUid") 3980 iData.pop("currentPrice") 3981 iData.pop("rawCalendar") 3982 3983 colNames = list(iData.keys()) 3984 if bonds is None: 3985 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3986 3987 else: 3988 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3989 3990 else: 3991 uLogger.warning("Instrument is not a bond!") 3992 3993 processed = round(100 * (i + 1) / iCount, 1) 3994 if tooLong and processed % 5 == 0: 3995 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3996 3997 else: 3998 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3999 4000 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4001 4002 # Saving bonds from Pandas DataFrame to XLSX sheet: 4003 if xlsx and self.bondsXLSXFile: 4004 with pd.ExcelWriter( 4005 path=self.bondsXLSXFile, 4006 date_format=TKS_DATE_FORMAT, 4007 datetime_format=TKS_DATE_TIME_FORMAT, 4008 mode="w", 4009 ) as writer: 4010 bonds.to_excel( 4011 writer, 4012 sheet_name="Extended bonds data", 4013 index=True, 4014 encoding="UTF-8", 4015 freeze_panes=(1, 1), 4016 ) # saving as XLSX-file with freeze first row and column as headers 4017 4018 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4019 4020 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4022 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4023 """ 4024 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4025 4026 WARNING! This is too long operation if a lot of bonds requested from broker server. 4027 4028 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4029 4030 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4031 extended information about bonds: main info, current prices, bond payment calendar, 4032 coupon yields, current yields and some statistics etc. 4033 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4034 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4035 for further used by data scientists or stock analytics. 4036 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4037 """ 4038 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4039 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4040 4041 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4042 4043 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4044 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4045 calendar = None 4046 for bond in extBonds.iterrows(): 4047 for item in bond[1]["calendar"]: 4048 cData = { 4049 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4050 "couponDate": item["couponDate"], 4051 "figi": bond[1]["figi"], 4052 "ticker": bond[1]["ticker"], 4053 "name": bond[1]["name"], 4054 "couponNumber": item["couponNumber"], 4055 "payOneBond": item["payOneBond"], 4056 "payCurrency": item["payCurrency"], 4057 "couponType": item["couponType"], 4058 "couponPeriod": item["couponPeriod"], 4059 "fixDate": item["fixDate"], 4060 "couponStartDate": item["couponStartDate"], 4061 "couponEndDate": item["couponEndDate"], 4062 } 4063 4064 if calendar is None: 4065 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4066 4067 else: 4068 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4069 4070 if calendar is not None: 4071 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4072 4073 # Saving calendar from Pandas DataFrame to XLSX sheet: 4074 if xlsx: 4075 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4076 4077 with pd.ExcelWriter( 4078 path=xlsxCalendarFile, 4079 date_format=TKS_DATE_FORMAT, 4080 datetime_format=TKS_DATE_TIME_FORMAT, 4081 mode="w", 4082 ) as writer: 4083 humanReadable = calendar.copy(deep=True) 4084 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4085 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4086 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4087 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4088 humanReadable.columns = colNames # human-readable column names 4089 4090 humanReadable.to_excel( 4091 writer, 4092 sheet_name="Bond payments calendar", 4093 index=False, 4094 encoding="UTF-8", 4095 freeze_panes=(1, 2), 4096 ) # saving as XLSX-file with freeze first row and column as headers 4097 4098 del humanReadable # release df in memory 4099 4100 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4101 4102 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4104 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4105 """ 4106 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4107 Also, creates Markdown file with calendar data, `calendar.md` by default. 4108 4109 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4110 4111 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4112 extended information about bonds: main info, current prices, bond payment calendar, 4113 coupon yields, current yields and some statistics etc. 4114 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4115 :param show: if `True` then also printing bonds payment calendar to the console, 4116 otherwise save to file `calendarFile` only. `False` by default. 4117 :return: multilines text in Markdown format with bonds payment calendar as a table. 4118 """ 4119 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4120 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4121 4122 infoText = "# Bond payments calendar\n\n" 4123 4124 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4125 4126 if not (calendar is None or calendar.empty): 4127 splitLine = "| | | | | | | | | |\n" 4128 4129 info = [ 4130 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4131 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4132 ] 4133 4134 newMonth = False 4135 notOneBond = calendar["figi"].nunique() > 1 4136 for i, bond in enumerate(calendar.iterrows()): 4137 if newMonth and notOneBond: 4138 info.append(splitLine) 4139 4140 info.append( 4141 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4142 " √" if bond[1]["paid"] else " —", 4143 bond[1]["couponDate"].split("T")[0], 4144 bond[1]["figi"], 4145 bond[1]["ticker"], 4146 bond[1]["couponNumber"], 4147 "{} {}".format( 4148 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4149 bond[1]["payCurrency"], 4150 ), 4151 bond[1]["couponType"], 4152 bond[1]["couponPeriod"], 4153 bond[1]["fixDate"].split("T")[0], 4154 ) 4155 ) 4156 4157 if i < len(calendar.values) - 1: 4158 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4159 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4160 newMonth = False if curDate.month == nextDate.month else True 4161 4162 else: 4163 newMonth = False 4164 4165 infoText += "".join(info) 4166 4167 if show: 4168 uLogger.info("{}".format(infoText)) 4169 4170 if self.calendarFile is not None: 4171 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4172 fH.write(infoText) 4173 4174 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4175 4176 else: 4177 infoText += "No data\n" 4178 4179 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4181 def OverviewAccounts(self, show: bool = False) -> dict: 4182 """ 4183 Method for parsing and show simple table with all available user accounts. 4184 4185 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4186 4187 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4188 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4189 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4190 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4191 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4192 "closed": "—", "access": "Full access" }, ...}}` 4193 """ 4194 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4195 4196 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4197 accounts = { 4198 item["id"]: { 4199 "type": TKS_ACCOUNT_TYPES[item["type"]], 4200 "name": item["name"], 4201 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4202 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4203 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4204 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4205 } for item in rawAccounts["accounts"] 4206 } 4207 4208 # Raw and parsed data with some fields replaced in "stat" section: 4209 view = { 4210 "rawAccounts": rawAccounts, 4211 "stat": accounts, 4212 } 4213 4214 # --- Prepare simple text table with only accounts data in human-readable format: 4215 if show: 4216 info = [ 4217 "# User accounts\n\n", 4218 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4219 "| Account ID | Type | Status | Name |\n", 4220 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4221 ] 4222 4223 for account in view["stat"].keys(): 4224 info.extend([ 4225 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4226 account, 4227 view["stat"][account]["type"], 4228 view["stat"][account]["status"], 4229 view["stat"][account]["name"], 4230 ) 4231 ]) 4232 4233 infoText = "".join(info) 4234 4235 uLogger.info(infoText) 4236 4237 if self.userAccountsFile: 4238 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4239 fH.write(infoText) 4240 4241 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4242 4243 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4245 def OverviewUserInfo(self, show: bool = False) -> dict: 4246 """ 4247 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4248 4249 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4250 4251 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4252 :return: dict with raw parsed data from server and some calculated statistics about it. 4253 """ 4254 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4255 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4256 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4257 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4258 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4259 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4260 4261 # This is dict with parsed common user data: 4262 userInfo = { 4263 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4264 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4265 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4266 "tariff": rawUserInfo["tariff"], 4267 } 4268 4269 # This is an array of dict with parsed margin statuses for every account IDs: 4270 margins = {} 4271 for accountId in accounts.keys(): 4272 if rawMargins[accountId]: 4273 margins[accountId] = { 4274 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4275 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4276 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4277 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4278 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4279 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4280 } 4281 4282 else: 4283 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4284 4285 unary = {} # unary-connection limits 4286 for item in rawTariffLimits["unaryLimits"]: 4287 if item["limitPerMinute"] in unary.keys(): 4288 unary[item["limitPerMinute"]].extend(item["methods"]) 4289 4290 else: 4291 unary[item["limitPerMinute"]] = item["methods"] 4292 4293 stream = {} # stream-connection limits 4294 for item in rawTariffLimits["streamLimits"]: 4295 if item["limit"] in stream.keys(): 4296 stream[item["limit"]].extend(item["streams"]) 4297 4298 else: 4299 stream[item["limit"]] = item["streams"] 4300 4301 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4302 limits = { 4303 "unary": unary, 4304 "stream": stream, 4305 } 4306 4307 # Raw and parsed data as an output result: 4308 view = { 4309 "rawUserInfo": rawUserInfo, 4310 "rawAccounts": rawAccounts, 4311 "rawMargins": rawMargins, 4312 "rawTariffLimits": rawTariffLimits, 4313 "stat": { 4314 "userInfo": userInfo, 4315 "accounts": accounts, 4316 "margins": margins, 4317 "limits": limits, 4318 }, 4319 } 4320 4321 # --- Prepare text table with user information in human-readable format: 4322 if show: 4323 info = [ 4324 "# Full user information\n\n", 4325 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4326 "## Common information\n\n", 4327 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4328 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4329 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4330 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4331 "\n## User accounts\n\n", 4332 ] 4333 4334 for account in view["stat"]["accounts"].keys(): 4335 info.extend([ 4336 "### ID: [{}]\n\n".format(account), 4337 "| Parameters | Values |\n", 4338 "|----------------------|--------------------------------------------------------------|\n", 4339 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4340 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4341 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4342 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4343 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4344 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4345 ]) 4346 4347 if margins[account]: 4348 info.extend([ 4349 "| Margin status: | Enabled |\n", 4350 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4351 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4352 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4353 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4354 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4355 ]) 4356 4357 else: 4358 info.append("| Margin status: | Disabled |\n\n") 4359 4360 info.extend([ 4361 "\n## Current user tariff limits\n", 4362 "\nSee also:\n", 4363 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4364 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4365 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4366 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4367 "\n### Unary limits\n", 4368 ]) 4369 4370 if unary: 4371 for key, values in sorted(unary.items()): 4372 info.append("\n* Max requests per minute: {}\n".format(key)) 4373 4374 for value in values: 4375 info.append(" - {}\n".format(value)) 4376 4377 else: 4378 info.append("\nNot available\n") 4379 4380 info.append("\n### Stream limits\n") 4381 4382 if stream: 4383 for key, values in sorted(stream.items()): 4384 info.append("\n* Max stream connections: {}\n".format(key)) 4385 4386 for value in values: 4387 info.append(" - {}\n".format(value)) 4388 4389 else: 4390 info.append("\nNot available\n") 4391 4392 infoText = "".join(info) 4393 4394 uLogger.info(infoText) 4395 4396 if self.userInfoFile: 4397 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4398 fH.write(infoText) 4399 4400 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4401 4402 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4405class Args: 4406 """ 4407 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4408 """ 4409 def __init__(self, **kwargs): 4410 self.__dict__.update(kwargs) 4411 4412 def __getattr__(self, item): 4413 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4416def ParseArgs(): 4417 """This function get and parse command line keys.""" 4418 parser = ArgumentParser() # command-line string parser 4419 4420 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4421 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4422 4423 # --- options: 4424 4425 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4426 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4427 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4428 4429 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4430 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4431 4432 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4433 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4434 4435 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4436 4437 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4438 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4439 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4440 4441 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4442 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4443 4444 # --- commands: 4445 4446 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4447 4448 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4449 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4450 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4451 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4452 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4453 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4454 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4455 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4456 4457 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4458 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4459 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4460 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4461 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4462 4463 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4464 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4465 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4466 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4467 4468 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4469 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4470 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4471 4472 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4473 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4474 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4475 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4476 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4477 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4478 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4479 4480 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4481 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4482 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4483 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4484 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4485 4486 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4487 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4488 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4489 4490 cmdArgs = parser.parse_args() 4491 return cmdArgs
This function get and parse command line keys.
4494def Main(**kwargs): 4495 """ 4496 Main function for work with TKSBrokerAPI in the console. 4497 4498 See examples: 4499 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4500 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4501 """ 4502 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4503 4504 if args.debug_level: 4505 uLogger.level = 10 # always debug level by default 4506 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4507 4508 exitCode = 0 4509 start = datetime.now(tzutc()) 4510 uLogger.debug("=-" * 60) 4511 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4512 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4513 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4514 )) 4515 4516 # trying to calculate full current version: 4517 buildVersion = __version__ 4518 try: 4519 v = version("tksbrokerapi") 4520 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4521 4522 except Exception: 4523 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4524 4525 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4526 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4527 4528 try: 4529 if args.version: 4530 print("TKSBrokerAPI {}".format(buildVersion)) 4531 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4532 4533 else: 4534 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4535 server = TinkoffBrokerServer( 4536 token=args.token, 4537 accountId=args.account_id, 4538 useCache=not args.no_cache, 4539 ) 4540 4541 # --- set some options: 4542 4543 if args.more: 4544 server.moreDebug = True 4545 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4546 4547 if args.ticker: 4548 if args.ticker in server.aliasesKeys: 4549 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4550 4551 else: 4552 server.ticker = args.ticker 4553 4554 if args.figi: 4555 server.figi = args.figi 4556 4557 if args.depth is not None: 4558 server.depth = args.depth 4559 4560 # --- do one command: 4561 4562 if args.list: 4563 if args.output is not None: 4564 server.instrumentsFile = args.output 4565 4566 server.ShowInstrumentsInfo(show=True) 4567 4568 elif args.list_xlsx: 4569 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4570 4571 elif args.bonds_xlsx is not None: 4572 if args.output is not None: 4573 server.bondsXLSXFile = args.output 4574 4575 if len(args.bonds_xlsx) == 0: 4576 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4577 4578 else: 4579 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4580 4581 elif args.search: 4582 if args.output is not None: 4583 server.searchResultsFile = args.output 4584 4585 server.SearchInstruments(pattern=args.search[0], show=True) 4586 4587 elif args.info: 4588 if not (args.ticker or args.figi): 4589 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4590 raise Exception("Ticker or FIGI required") 4591 4592 if args.output is not None: 4593 server.infoFile = args.output 4594 4595 if args.ticker: 4596 server.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4597 4598 else: 4599 server.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4600 4601 elif args.calendar is not None: 4602 if args.output is not None: 4603 server.calendarFile = args.output 4604 4605 if len(args.calendar) == 0: 4606 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4607 4608 else: 4609 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4610 4611 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4612 4613 elif args.price: 4614 if not (args.ticker or args.figi): 4615 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4616 raise Exception("Ticker or FIGI required") 4617 4618 server.GetCurrentPrices(show=True) 4619 4620 elif args.prices is not None: 4621 if args.output is not None: 4622 server.pricesFile = args.output 4623 4624 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4625 4626 elif args.overview: 4627 if args.output is not None: 4628 server.overviewFile = args.output 4629 4630 server.Overview(show=True, details="full") 4631 4632 elif args.overview_digest: 4633 if args.output is not None: 4634 server.overviewDigestFile = args.output 4635 4636 server.Overview(show=True, details="digest") 4637 4638 elif args.overview_positions: 4639 if args.output is not None: 4640 server.overviewPositionsFile = args.output 4641 4642 server.Overview(show=True, details="positions") 4643 4644 elif args.overview_orders: 4645 if args.output is not None: 4646 server.overviewOrdersFile = args.output 4647 4648 server.Overview(show=True, details="orders") 4649 4650 elif args.overview_analytics: 4651 if args.output is not None: 4652 server.overviewAnalyticsFile = args.output 4653 4654 server.Overview(show=True, details="analytics") 4655 4656 elif args.deals is not None: 4657 if args.output is not None: 4658 server.reportFile = args.output 4659 4660 if 0 <= len(args.deals) < 3: 4661 server.Deals( 4662 start=args.deals[0] if len(args.deals) >= 1 else None, 4663 end=args.deals[1] if len(args.deals) == 2 else None, 4664 show=True, # Always show deals report in console 4665 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4666 ) 4667 4668 else: 4669 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4670 raise Exception("Incorrect value") 4671 4672 elif args.history is not None: 4673 if args.output is not None: 4674 server.historyFile = args.output 4675 4676 if 0 <= len(args.history) < 3: 4677 dataReceived = server.History( 4678 start=args.history[0] if len(args.history) >= 1 else None, 4679 end=args.history[1] if len(args.history) == 2 else None, 4680 interval="hour" if args.interval is None or not args.interval else args.interval, 4681 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4682 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4683 show=True, # shows all downloaded candles in console 4684 ) 4685 4686 if args.render_chart is not None and dataReceived is not None: 4687 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4688 4689 server.ShowHistoryChart( 4690 candles=dataReceived, 4691 interact=iChart, 4692 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4693 ) 4694 4695 else: 4696 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4697 raise Exception("Incorrect value") 4698 4699 elif args.load_history is not None: 4700 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4701 4702 if args.render_chart is not None and histData is not None: 4703 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4704 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4705 4706 server.ShowHistoryChart( 4707 candles=histData, 4708 interact=iChart, 4709 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4710 ) 4711 4712 elif args.trade is not None: 4713 if 1 <= len(args.trade) <= 5: 4714 server.Trade( 4715 operation=args.trade[0], 4716 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4717 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4718 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4719 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4720 ) 4721 4722 else: 4723 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4724 4725 elif args.buy is not None: 4726 if 0 <= len(args.buy) <= 4: 4727 server.Buy( 4728 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4729 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4730 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4731 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4732 ) 4733 4734 else: 4735 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4736 4737 elif args.sell is not None: 4738 if 0 <= len(args.sell) <= 4: 4739 server.Sell( 4740 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4741 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4742 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4743 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4744 ) 4745 4746 else: 4747 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4748 4749 elif args.order: 4750 if 4 <= len(args.order) <= 7: 4751 server.Order( 4752 operation=args.order[0], 4753 orderType=args.order[1], 4754 lots=int(args.order[2]), 4755 targetPrice=float(args.order[3]), 4756 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4757 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4758 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4759 ) 4760 4761 else: 4762 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4763 4764 elif args.buy_limit: 4765 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4766 4767 elif args.sell_limit: 4768 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4769 4770 elif args.buy_stop: 4771 if 2 <= len(args.buy_stop) <= 7: 4772 server.BuyStop( 4773 lots=int(args.buy_stop[0]), 4774 targetPrice=float(args.buy_stop[1]), 4775 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4776 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4777 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4778 ) 4779 4780 else: 4781 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4782 4783 elif args.sell_stop: 4784 if 2 <= len(args.sell_stop) <= 7: 4785 server.SellStop( 4786 lots=int(args.sell_stop[0]), 4787 targetPrice=float(args.sell_stop[1]), 4788 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4789 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4790 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4791 ) 4792 4793 else: 4794 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4795 4796 # elif args.buy_order_grid is not None: 4797 # # update order grid work with api v2 4798 # if len(args.buy_order_grid) == 2: 4799 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4800 # 4801 # for order in orderParams: 4802 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4803 # 4804 # else: 4805 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4806 # 4807 # elif args.sell_order_grid is not None: 4808 # # update order grid work with api v2 4809 # if len(args.sell_order_grid) >= 2: 4810 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4811 # 4812 # for order in orderParams: 4813 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4814 # 4815 # else: 4816 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4817 4818 elif args.close_order is not None: 4819 server.CloseOrders(args.close_order) # close only one order 4820 4821 elif args.close_orders is not None: 4822 server.CloseOrders(args.close_orders) # close list of orders 4823 4824 elif args.close_trade: 4825 if not (args.ticker or args.figi): 4826 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4827 raise Exception("Ticker or FIGI required") 4828 4829 if args.ticker: 4830 server.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4831 4832 else: 4833 server.CloseTrades([args.figi]) # close only one trade by FIGI 4834 4835 elif args.close_trades is not None: 4836 server.CloseTrades(args.close_trades) # close trades for list of tickers 4837 4838 elif args.close_all is not None: 4839 server.CloseAll(*args.close_all) 4840 4841 elif args.limits: 4842 if args.output is not None: 4843 server.withdrawalLimitsFile = args.output 4844 4845 server.OverviewLimits(show=True) 4846 4847 elif args.user_info: 4848 if args.output is not None: 4849 server.userInfoFile = args.output 4850 4851 server.OverviewUserInfo(show=True) 4852 4853 elif args.account: 4854 if args.output is not None: 4855 server.userAccountsFile = args.output 4856 4857 server.OverviewAccounts(show=True) 4858 4859 else: 4860 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4861 raise Exception("There is no command to execute") 4862 4863 except Exception: 4864 trace = tb.format_exc() 4865 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4866 if e in trace: 4867 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4868 break 4869 4870 uLogger.debug(trace) 4871 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4872 exitCode = 255 # an error occurred, must be open a ticket for this issue 4873 4874 finally: 4875 finish = datetime.now(tzutc()) 4876 4877 if exitCode == 0: 4878 if args.more: 4879 uLogger.debug("All operations were finished success (summary code is 0).") 4880 4881 else: 4882 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4883 os.path.abspath(uLog.defaultLogFile), exitCode, 4884 )) 4885 4886 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4887 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4888 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4889 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4890 )) 4891 uLogger.debug("=-" * 60) 4892 4893 if not kwargs: 4894 sys.exit(exitCode) 4895 4896 else: 4897 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: